diff --git a/DEPS b/DEPS
index 7f40e44..8ea41a2 100644
--- a/DEPS
+++ b/DEPS
@@ -209,11 +209,11 @@
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling Skia
   # and whatever else without interference from each other.
-  'skia_revision': '497bdf9599081c33bedea94f90a8be9b4bde84f5',
+  'skia_revision': 'd1de126c257b74d676ebdf1f11b7815b86d48500',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling V8
   # and whatever else without interference from each other.
-  'v8_revision': 'ff6dfae2642d1e8d7ca616058d3d16561078bc06',
+  'v8_revision': '246e3704f3f6607dacf22c24fb71b5d1e54ecc54',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling swarming_client
   # and whatever else without interference from each other.
@@ -328,7 +328,7 @@
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling feed
   # and whatever else without interference from each other.
-  'dawn_revision': '1257152485449816850604207ce189de6aa84db0',
+  'dawn_revision': 'b745df8537a008e041085a75224b94b80b320af9',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling feed
   # and whatever else without interference from each other.
@@ -368,7 +368,7 @@
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling feed
   # and whatever else without interference from each other.
-  'tint_revision': 'cea744d55887d4e47ae663d9fb70a417b348499e',
+  'tint_revision': '9ef17472e8719f15d35d1df842c1f074aaf8af5f',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling feed
   # and whatever else without interference from each other.
@@ -376,7 +376,7 @@
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling feed
   # and whatever else without interference from each other.
-  'libcxxabi_revision':    '9bb07683fbffe86edb42c5c519b7a4bc4e893deb',
+  'libcxxabi_revision':    'cbf9455e837f39dac89f9e3365692e9251019d4e',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling feed
   # and whatever else without interference from each other.
@@ -391,7 +391,7 @@
   'libcxx_revision':       '8fa87946779682841e21e2da977eccfb6cb3bded',
 
   # GN CIPD package version.
-  'gn_version': 'git_revision:64b3b9401c1c3ed5f3c43c1cac00b91f83597ab8',
+  'gn_version': 'git_revision:b2e3d8622c1ce1bd853c7a11f62a739946669cdd',
 }
 
 # Only these hosts are allowed for dependencies in this DEPS file.
@@ -632,15 +632,37 @@
   'src/net/third_party/quiche/src':
     Var('quiche_git') + '/quiche.git' + '@' +  Var('quiche_revision'),
 
-  'src/testing/rts': {
+  'src/testing/rts/linux-amd64': {
       'packages': [
         {
-          'package': 'chromium/rts/model/${{platform}}',
-          'version': 'latest',
+          'package': 'chromium/rts/model/linux-amd64',
+          'version': 'Jwj7x5-mH4gjDNBNh-QU2H3zdVSpbffIIzJAvGPSZzsC',
         },
       ],
       'dep_type': 'cipd',
-      'condition': 'checkout_rts_model',
+      'condition': 'checkout_rts_model and checkout_linux',
+  },
+
+  'src/testing/rts/mac-amd64': {
+      'packages': [
+        {
+          'package': 'chromium/rts/model/mac-amd64',
+          'version': 'vUbSV1P-bdoTVfesLnAyUCfWlr2uMk1ZzxMg-Eju6YUC',
+        },
+      ],
+      'dep_type': 'cipd',
+      'condition': 'checkout_rts_model and checkout_mac',
+  },
+
+  'src/testing/rts/windows-amd64': {
+      'packages': [
+        {
+          'package': 'chromium/rts/model/windows-amd64',
+          'version': 'qPS2sCiiuvr7ZljFS2dOOKoJCHeXZC8ILxoFR8g0Ve8C',
+        },
+      ],
+      'dep_type': 'cipd',
+      'condition': 'checkout_rts_model and checkout_win',
   },
 
   'src/tools/luci-go': {
@@ -701,7 +723,7 @@
     'packages': [
       {
           'package': 'chromium/third_party/androidx',
-          'version': 'HAFunKKkVFyBzh9p8f9RSwgNiB0ISkdp2WIbBR71FeMC',
+          'version': 'g8SLuoOc1bCcY1mN-J9JLpK6ha0jgDwjWRJqsDwEtM4C',
       },
     ],
     'condition': 'checkout_android',
@@ -914,7 +936,7 @@
   # Tools used when building Chrome for Chrome OS. This affects both the Simple
   # Chrome workflow, as well as the chromeos-chrome ebuild.
   'src/third_party/chromite': {
-      'url': Var('chromium_git') + '/chromiumos/chromite.git' + '@' + '369f0831a4ba22b56ddc5153a8ccbfa9948814b9',
+      'url': Var('chromium_git') + '/chromiumos/chromite.git' + '@' + 'd4fca1099e63b468a2de46bbc3eb21f32b884801',
       'condition': 'checkout_chromeos',
   },
 
@@ -1384,7 +1406,7 @@
       'packages': [
           {
               'package': 'fuchsia/third_party/aemu/linux-amd64',
-              'version': 'Q7BOLoBEKjKHVG4WYSf3qNuhySZ9nyW9T5qODWXbwLEC'
+              'version': 'PQ5BA4WJfsqBSdlGRVmglasbA61vdJ1sHFa_R-euwHQC'
           },
       ],
       'condition': 'host_os == "linux" and checkout_fuchsia',
@@ -1614,7 +1636,7 @@
     Var('chromium_git') + '/v8/v8.git' + '@' +  Var('v8_revision'),
 
   'src-internal': {
-    'url': 'https://chrome-internal.googlesource.com/chrome/src-internal.git@b821403a2ecfaf00d272d09aaf18905eebbc2e9d',
+    'url': 'https://chrome-internal.googlesource.com/chrome/src-internal.git@924f592fada7927f1b3c21774fa95ca33914a1fa',
     'condition': 'checkout_src_internal',
   },
 
@@ -1633,7 +1655,7 @@
     'packages': [
       {
         'package': 'chromeos_internal/apps/help_app/app',
-        'version': 'M4V26Rsq7u7TXGnu4QFqwKX4lgGJI8Sz0hmQTlDJltcC',
+        'version': 'vtDW7dBl0jvX3o9P-eUzDW1iH5bCIFH5Eh181hW2h18C',
       },
     ],
     'condition': 'checkout_chromeos and checkout_src_internal',
@@ -1644,7 +1666,7 @@
     'packages': [
       {
         'package': 'chromeos_internal/apps/media_app/app',
-        'version': 'ImuhLGH-ovEO8EjQznen9y4IyDaelrTUCfFyZCvSObQC',
+        'version': 'HerPX5kPiZcf3Z1X2Mr4q2qi9Y-8dCxlANBhoqU0Uf4C',
       },
     ],
     'condition': 'checkout_chromeos and checkout_src_internal',
diff --git a/android_webview/browser/gfx/browser_view_renderer.cc b/android_webview/browser/gfx/browser_view_renderer.cc
index 435e0d4..8412d0c 100644
--- a/android_webview/browser/gfx/browser_view_renderer.cc
+++ b/android_webview/browser/gfx/browser_view_renderer.cc
@@ -170,6 +170,13 @@
 void BrowserViewRenderer::TrimMemory() {
   DCHECK(ui_task_runner_->BelongsToCurrentThread());
   TRACE_EVENT0("android_webview", "BrowserViewRenderer::TrimMemory");
+
+  // Trimming memory might destroy HardwareRenderer which will evict
+  // CompositorFrame, compositor needs to submit next frames with new local
+  // surface id.
+  if (compositor_)
+    compositor_->WasEvicted();
+
   // Just set the memory limit to 0 and drop all tiles. This will be reset to
   // normal levels in the next DrawGL call.
   if (!offscreen_pre_raster_)
diff --git a/ash/constants/ash_features.cc b/ash/constants/ash_features.cc
index 46ba428..94d7dae 100644
--- a/ash/constants/ash_features.cc
+++ b/ash/constants/ash_features.cc
@@ -183,6 +183,10 @@
 const base::Feature kImeOptionsInSettings{"ImeOptionsInSettings",
                                           base::FEATURE_DISABLED_BY_DEFAULT};
 
+// Enables or disables Crosh System Web App. When enabled, crosh (Chrome OS
+// Shell) will run as a tabbed System Web App rather than a normal browser tab.
+const base::Feature kCroshSWA{"CroshSWA", base::FEATURE_DISABLED_BY_DEFAULT};
+
 // Enables or disables Crostini Disk Resizing.
 const base::Feature kCrostiniDiskResizing{"CrostiniDiskResizing",
                                           base::FEATURE_ENABLED_BY_DEFAULT};
diff --git a/ash/constants/ash_features.h b/ash/constants/ash_features.h
index 91a6b40..9622f32 100644
--- a/ash/constants/ash_features.h
+++ b/ash/constants/ash_features.h
@@ -96,6 +96,8 @@
 COMPONENT_EXPORT(ASH_CONSTANTS)
 extern const base::Feature kChildSpecificSignin;
 COMPONENT_EXPORT(ASH_CONSTANTS)
+extern const base::Feature kCroshSWA;
+COMPONENT_EXPORT(ASH_CONSTANTS)
 extern const base::Feature kCrostiniDiskResizing;
 COMPONENT_EXPORT(ASH_CONSTANTS)
 extern const base::Feature kCrostiniUseBusterImage;
diff --git a/build/fuchsia/linux.sdk.sha1 b/build/fuchsia/linux.sdk.sha1
index b33a5e0b..db9268b4 100644
--- a/build/fuchsia/linux.sdk.sha1
+++ b/build/fuchsia/linux.sdk.sha1
@@ -1 +1 @@
-3.20210326.1.1
+3.20210328.0.1
diff --git a/build/fuchsia/mac.sdk.sha1 b/build/fuchsia/mac.sdk.sha1
index b33a5e0b..db9268b4 100644
--- a/build/fuchsia/mac.sdk.sha1
+++ b/build/fuchsia/mac.sdk.sha1
@@ -1 +1 @@
-3.20210326.1.1
+3.20210328.0.1
diff --git a/buildtools/mac/clang-format.sha1 b/buildtools/mac/clang-format.sha1
index 5ef063b..feb8155 100644
--- a/buildtools/mac/clang-format.sha1
+++ b/buildtools/mac/clang-format.sha1
@@ -1 +1 @@
-62bde1baa7196ad9df969fc1f06b66360b1a927b
\ No newline at end of file
+ae6765c699ed32e9dca305645456dcdf5cda0438
\ No newline at end of file
diff --git a/chrome/VERSION b/chrome/VERSION
index 83f70ced..bba6b083 100644
--- a/chrome/VERSION
+++ b/chrome/VERSION
@@ -1,4 +1,4 @@
 MAJOR=91
 MINOR=0
-BUILD=4460
+BUILD=4462
 PATCH=0
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/app/tabmodel/CustomTabsTabModelOrchestrator.java b/chrome/android/java/src/org/chromium/chrome/browser/app/tabmodel/CustomTabsTabModelOrchestrator.java
index b319dbb..8cc4abd 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/app/tabmodel/CustomTabsTabModelOrchestrator.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/app/tabmodel/CustomTabsTabModelOrchestrator.java
@@ -8,6 +8,7 @@
 
 import org.chromium.base.supplier.Supplier;
 import org.chromium.chrome.browser.dependency_injection.ActivityScope;
+import org.chromium.chrome.browser.flags.ActivityType;
 import org.chromium.chrome.browser.tabmodel.AsyncTabParamsManager;
 import org.chromium.chrome.browser.tabmodel.NextTabPolicy;
 import org.chromium.chrome.browser.tabmodel.NextTabPolicy.NextTabPolicySupplier;
@@ -38,8 +39,8 @@
         // Instantiate TabModelSelectorImpl
         NextTabPolicySupplier nextTabPolicySupplier = () -> NextTabPolicy.LOCATIONAL;
         mTabModelSelector = new TabModelSelectorImpl(windowAndroidSupplier, tabCreatorManager,
-                tabModelFilterFactory, nextTabPolicySupplier, asyncTabParamsManager, false, false,
-                false);
+                tabModelFilterFactory, nextTabPolicySupplier, asyncTabParamsManager, false,
+                ActivityType.CUSTOM_TAB, false);
 
         // Instantiate TabPersistentStore
         mTabPersistentStore =
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/app/tabmodel/DefaultTabModelSelectorFactory.java b/chrome/android/java/src/org/chromium/chrome/browser/app/tabmodel/DefaultTabModelSelectorFactory.java
index 41f14d8..79a1cd39 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/app/tabmodel/DefaultTabModelSelectorFactory.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/app/tabmodel/DefaultTabModelSelectorFactory.java
@@ -7,6 +7,7 @@
 import android.app.Activity;
 
 import org.chromium.base.annotations.VerifiesOnN;
+import org.chromium.chrome.browser.flags.ActivityType;
 import org.chromium.chrome.browser.tabmodel.AsyncTabParamsManager;
 import org.chromium.chrome.browser.tabmodel.NextTabPolicy.NextTabPolicySupplier;
 import org.chromium.chrome.browser.tabmodel.TabCreatorManager;
@@ -29,7 +30,7 @@
         AsyncTabParamsManager asyncTabParamsManager = AsyncTabParamsManagerSingleton.getInstance();
 
         return new TabModelSelectorImpl(/*windowAndroidSupplier=*/null, tabCreatorManager,
-                tabModelFilterFactory, nextTabPolicySupplier, asyncTabParamsManager, true, true,
-                false);
+                tabModelFilterFactory, nextTabPolicySupplier, asyncTabParamsManager, true,
+                ActivityType.TABBED, false);
     }
 }
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/omnibox/ChromeAutocompleteProviderClient.java b/chrome/android/java/src/org/chromium/chrome/browser/omnibox/ChromeAutocompleteProviderClient.java
index a352bf0..4cf5bd1 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/omnibox/ChromeAutocompleteProviderClient.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/omnibox/ChromeAutocompleteProviderClient.java
@@ -6,7 +6,6 @@
 
 import org.chromium.base.annotations.CalledByNative;
 import org.chromium.chrome.browser.tab.Tab;
-import org.chromium.chrome.browser.tab.TabImpl;
 import org.chromium.chrome.browser.tabmodel.TabModel;
 
 import java.util.ArrayList;
@@ -17,27 +16,18 @@
  */
 public class ChromeAutocompleteProviderClient {
     @CalledByNative
-    private static TabImpl[] getAllHiddenAndNonCCTTabs(TabModel[] tabModels) {
+    private static Tab[] getAllHiddenTabs(TabModel[] tabModels) {
         if (tabModels == null) return null;
-        List<Tab> tabList = new ArrayList<Tab>();
+        List<Tab> tabList = new ArrayList<>();
 
         for (TabModel tabModel : tabModels) {
             if (tabModel == null) continue;
+
             for (int i = 0; i < tabModel.getCount(); ++i) {
-                TabImpl tab = (TabImpl) tabModel.getTabAt(i);
-                if (tab.isHidden() && !tab.isCustomTab()) {
-                    tabList.add(tab);
-                }
+                Tab tab = tabModel.getTabAt(i);
+                if (tab.isHidden()) tabList.add(tab);
             }
         }
-        if (tabList.size() == 0) {
-            return null;
-        }
-
-        TabImpl[] tabImplArray = new TabImpl[tabList.size()];
-        for (int i = 0; i < tabList.size(); i++) {
-            tabImplArray[i] = (TabImpl) tabList.get(i);
-        }
-        return tabImplArray;
+        return tabList.isEmpty() ? null : tabList.toArray(new Tab[0]);
     }
 }
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/omnibox/DEPS b/chrome/android/java/src/org/chromium/chrome/browser/omnibox/DEPS
index d801e9f7..9b5138c 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/omnibox/DEPS
+++ b/chrome/android/java/src/org/chromium/chrome/browser/omnibox/DEPS
@@ -14,7 +14,6 @@
   "+chrome/android/java/src/org/chromium/chrome/browser/privacy/settings/PrivacyPreferencesManagerImpl.java",
   "+chrome/android/java/src/org/chromium/chrome/browser/share/ShareDelegate.java",
   "+chrome/android/java/src/org/chromium/chrome/browser/share/ShareDelegateImpl.java",
-  "+chrome/android/java/src/org/chromium/chrome/browser/tab/TabImpl.java",
   "+chrome/android/java/src/org/chromium/chrome/browser/WarmupManager.java",
 
   "+base/android/java/src/org/chromium/base",
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/tab/TabImpl.java b/chrome/android/java/src/org/chromium/chrome/browser/tab/TabImpl.java
index 03a5e45..3d82de0 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/tab/TabImpl.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/tab/TabImpl.java
@@ -1334,12 +1334,12 @@
     }
 
     @CalledByNative
-    private static long[] getAllNativePtrs(TabImpl[] tabsArray) {
+    private static long[] getAllNativePtrs(Tab[] tabsArray) {
         if (tabsArray == null) return null;
 
         long[] tabsPtrArray = new long[tabsArray.length];
         for (int i = 0; i < tabsArray.length; i++) {
-            tabsPtrArray[i] = tabsArray[i].getNativePtr();
+            tabsPtrArray[i] = ((TabImpl) tabsArray[i]).getNativePtr();
         }
         return tabsPtrArray;
     }
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/tabmodel/IncognitoTabModelImplCreator.java b/chrome/android/java/src/org/chromium/chrome/browser/tabmodel/IncognitoTabModelImplCreator.java
index a39810f..11b728bb 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/tabmodel/IncognitoTabModelImplCreator.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/tabmodel/IncognitoTabModelImplCreator.java
@@ -11,6 +11,7 @@
 
 import org.chromium.base.supplier.Supplier;
 import org.chromium.chrome.browser.compositor.layouts.content.TabContentManager;
+import org.chromium.chrome.browser.flags.ActivityType;
 import org.chromium.chrome.browser.profiles.Profile;
 import org.chromium.chrome.browser.tabmodel.IncognitoTabModelImpl.IncognitoTabModelDelegate;
 import org.chromium.chrome.browser.tabmodel.NextTabPolicy.NextTabPolicySupplier;
@@ -33,6 +34,7 @@
     @Nullable
     private final Supplier<WindowAndroid> mWindowAndroidSupplier;
 
+    private final @ActivityType int mActivityType;
     /**
      * Constructor for an IncognitoTabModelImplCreator, used by {@link IncognitoTabModelImpl}.
      *
@@ -47,13 +49,15 @@
      * @param tabContentManager   Manages the display content of the tab.
      * @param nextTabPolicySupplier Supplies the policy to pick a next tab if the current is closed
      * @param asyncTabParamsManager An {@link AsyncTabParamsManager} instance.
-     * @param modelDelegate       Delegate to handle external dependencies and interactions.
+     * @param activityType Type of the activity for the tab model.
+     * @param modelDelegate Delegate to handle external dependencies and interactions.
      */
     IncognitoTabModelImplCreator(@Nullable Supplier<WindowAndroid> windowAndroidSupplier,
             TabCreator regularTabCreator, TabCreator incognitoTabCreator,
             TabModelOrderController orderController, TabContentManager tabContentManager,
             NextTabPolicySupplier nextTabPolicySupplier,
-            AsyncTabParamsManager asyncTabParamsManager, TabModelDelegate modelDelegate) {
+            AsyncTabParamsManager asyncTabParamsManager, @ActivityType int activityType,
+            TabModelDelegate modelDelegate) {
         mWindowAndroidSupplier = windowAndroidSupplier;
         mRegularTabCreator = regularTabCreator;
         mIncognitoTabCreator = incognitoTabCreator;
@@ -61,6 +65,7 @@
         mTabContentManager = tabContentManager;
         mNextTabPolicySupplier = nextTabPolicySupplier;
         mAsyncTabParamsManager = asyncTabParamsManager;
+        mActivityType = activityType;
         mModelDelegate = modelDelegate;
     }
 
@@ -81,9 +86,8 @@
 
     @Override
     public TabModel createTabModel() {
-        Profile otrProfile = getOTRProfile();
-        return new TabModelImpl(otrProfile, false, mRegularTabCreator, mIncognitoTabCreator,
-                mOrderController, mTabContentManager, mNextTabPolicySupplier,
+        return new TabModelImpl(getOTRProfile(), mActivityType, mRegularTabCreator,
+                mIncognitoTabCreator, mOrderController, mTabContentManager, mNextTabPolicySupplier,
                 mAsyncTabParamsManager, mModelDelegate, false);
     }
 
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/tabmodel/TabModelImpl.java b/chrome/android/java/src/org/chromium/chrome/browser/tabmodel/TabModelImpl.java
index d1f3b0db..4e5a1c27 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/tabmodel/TabModelImpl.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/tabmodel/TabModelImpl.java
@@ -13,6 +13,7 @@
 import org.chromium.base.metrics.RecordUserAction;
 import org.chromium.chrome.browser.ChromeTabbedActivity;
 import org.chromium.chrome.browser.compositor.layouts.content.TabContentManager;
+import org.chromium.chrome.browser.flags.ActivityType;
 import org.chromium.chrome.browser.homepage.HomepageManager;
 import org.chromium.chrome.browser.ntp.RecentlyClosedBridge;
 import org.chromium.chrome.browser.profiles.Profile;
@@ -85,13 +86,13 @@
     private boolean mIsUndoSupported = true;
     private boolean mActive;
 
-    public TabModelImpl(@NonNull Profile profile, boolean isTabbedActivity,
+    public TabModelImpl(@NonNull Profile profile, @ActivityType int activityType,
             TabCreator regularTabCreator, TabCreator incognitoTabCreator,
             TabModelOrderController orderController, TabContentManager tabContentManager,
             NextTabPolicySupplier nextTabPolicySupplier,
             AsyncTabParamsManager asyncTabParamsManager, TabModelDelegate modelDelegate,
             boolean supportUndo) {
-        super(profile, isTabbedActivity);
+        super(profile, activityType);
         mRegularTabCreator = regularTabCreator;
         mIncognitoTabCreator = incognitoTabCreator;
         mOrderController = orderController;
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/tabmodel/TabModelJniBridge.java b/chrome/android/java/src/org/chromium/chrome/browser/tabmodel/TabModelJniBridge.java
index b321d8b..c947c2e 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/tabmodel/TabModelJniBridge.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/tabmodel/TabModelJniBridge.java
@@ -11,6 +11,7 @@
 import org.chromium.base.annotations.CalledByNative;
 import org.chromium.base.annotations.NativeMethods;
 import org.chromium.base.supplier.ObservableSupplier;
+import org.chromium.chrome.browser.flags.ActivityType;
 import org.chromium.chrome.browser.profiles.Profile;
 import org.chromium.chrome.browser.tab.Tab;
 import org.chromium.chrome.browser.tab.TabLaunchType;
@@ -28,25 +29,22 @@
 public abstract class TabModelJniBridge implements TabModel {
     private final boolean mIsIncognito;
 
+    /** The type of the Activity for which this tab model works. */
+    private final @ActivityType int mActivityType;
+
     /** Native TabModelJniBridge pointer, which will be set by {@link #initializeNative()}. */
     private long mNativeTabModelJniBridge;
 
-    /**
-     * Whether this tab model is part of a tabbed activity.
-     * This is consumed by Sync as part of restoring sync data from a previous session.
-     */
-    private boolean mIsTabbedActivityForSync;
-
-    public TabModelJniBridge(@NonNull Profile profile, boolean isTabbedActivity) {
+    public TabModelJniBridge(@NonNull Profile profile, @ActivityType int activityType) {
         mIsIncognito = profile.isOffTheRecord();
-        mIsTabbedActivityForSync = isTabbedActivity;
+        mActivityType = activityType;
     }
 
     /** Initializes the native-side counterpart to this class. */
     protected void initializeNative(Profile profile) {
         assert mNativeTabModelJniBridge == 0;
-        mNativeTabModelJniBridge = TabModelJniBridgeJni.get().init(
-                TabModelJniBridge.this, profile, mIsTabbedActivityForSync);
+        mNativeTabModelJniBridge =
+                TabModelJniBridgeJni.get().init(TabModelJniBridge.this, profile, mActivityType);
     }
 
     /** @return Whether the native-side pointer has been initialized. */
@@ -197,7 +195,7 @@
     @NativeMethods
     @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
     public interface Natives {
-        long init(TabModelJniBridge caller, Profile profile, boolean isTabbedActivity);
+        long init(TabModelJniBridge caller, Profile profile, @ActivityType int activityType);
         Profile getProfileAndroid(long nativeTabModelJniBridge, TabModelJniBridge caller);
         void broadcastSessionRestoreComplete(
                 long nativeTabModelJniBridge, TabModelJniBridge caller);
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/tabmodel/TabModelSelectorImpl.java b/chrome/android/java/src/org/chromium/chrome/browser/tabmodel/TabModelSelectorImpl.java
index aa13c8d..607a948 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/tabmodel/TabModelSelectorImpl.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/tabmodel/TabModelSelectorImpl.java
@@ -11,6 +11,7 @@
 
 import org.chromium.base.supplier.Supplier;
 import org.chromium.chrome.browser.compositor.layouts.content.TabContentManager;
+import org.chromium.chrome.browser.flags.ActivityType;
 import org.chromium.chrome.browser.profiles.Profile;
 import org.chromium.chrome.browser.tab.SadTab;
 import org.chromium.chrome.browser.tab.Tab;
@@ -38,9 +39,9 @@
 
     private boolean mIsUndoSupported;
 
-    // Whether the Activity that owns that TabModelSelector is tabbed or not.
-    // Used by sync to determine how to handle restore on cold start.
-    private boolean mIsTabbedActivityForSync;
+    // Type of the Activity for this tab model. Used by sync to determine how to handle restore
+    // on cold start.
+    private final @ActivityType int mActivityType;
 
     private final TabModelOrderController mOrderController;
 
@@ -64,20 +65,21 @@
      * @param tabModelFilterFactory
      * @param nextTabPolicySupplier
      * @param asyncTabParamsManager
+     * @param activityType Type of the activity for the tab model selector.
      * @param supportUndo Whether a tab closure can be undone.
      */
     public TabModelSelectorImpl(@Nullable Supplier<WindowAndroid> windowAndroidSupplier,
             TabCreatorManager tabCreatorManager, TabModelFilterFactory tabModelFilterFactory,
             NextTabPolicySupplier nextTabPolicySupplier,
             AsyncTabParamsManager asyncTabParamsManager, boolean supportUndo,
-            boolean isTabbedActivity, boolean startIncognito) {
+            @ActivityType int activityType, boolean startIncognito) {
         super(tabCreatorManager, tabModelFilterFactory, startIncognito);
         mWindowAndroidSupplier = windowAndroidSupplier;
         mIsUndoSupported = supportUndo;
-        mIsTabbedActivityForSync = isTabbedActivity;
         mOrderController = new TabModelOrderControllerImpl(this);
         mNextTabPolicySupplier = nextTabPolicySupplier;
         mAsyncTabParamsManager = asyncTabParamsManager;
+        mActivityType = activityType;
     }
 
     @Override
@@ -115,15 +117,15 @@
         ChromeTabCreator incognitoTabCreator =
                 (ChromeTabCreator) getTabCreatorManager().getTabCreator(true);
         TabModelImpl normalModel = new TabModelImpl(Profile.getLastUsedRegularProfile(),
-                mIsTabbedActivityForSync, regularTabCreator, incognitoTabCreator, mOrderController,
+                mActivityType, regularTabCreator, incognitoTabCreator, mOrderController,
                 mTabContentManager, mNextTabPolicySupplier, mAsyncTabParamsManager, this,
                 mIsUndoSupported);
         regularTabCreator.setTabModel(normalModel, mOrderController);
 
-        IncognitoTabModel incognitoModel =
-                new IncognitoTabModelImpl(new IncognitoTabModelImplCreator(mWindowAndroidSupplier,
-                        regularTabCreator, incognitoTabCreator, mOrderController,
-                        mTabContentManager, mNextTabPolicySupplier, mAsyncTabParamsManager, this));
+        IncognitoTabModel incognitoModel = new IncognitoTabModelImpl(
+                new IncognitoTabModelImplCreator(mWindowAndroidSupplier, regularTabCreator,
+                        incognitoTabCreator, mOrderController, mTabContentManager,
+                        mNextTabPolicySupplier, mAsyncTabParamsManager, mActivityType, this));
         incognitoTabCreator.setTabModel(incognitoModel, mOrderController);
         onNativeLibraryReadyInternal(tabContentProvider, normalModel, incognitoModel);
     }
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/tabmodel/ContextMenuLoadUrlParamsTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/tabmodel/ContextMenuLoadUrlParamsTest.java
index c4be5334..62215b6 100644
--- a/chrome/android/javatests/src/org/chromium/chrome/browser/tabmodel/ContextMenuLoadUrlParamsTest.java
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/tabmodel/ContextMenuLoadUrlParamsTest.java
@@ -26,6 +26,7 @@
 import org.chromium.chrome.browser.app.tabmodel.ChromeTabModelFilterFactory;
 import org.chromium.chrome.browser.app.tabmodel.TabWindowManagerSingleton;
 import org.chromium.chrome.browser.firstrun.FirstRunStatus;
+import org.chromium.chrome.browser.flags.ActivityType;
 import org.chromium.chrome.browser.flags.ChromeSwitches;
 import org.chromium.chrome.browser.tab.Tab;
 import org.chromium.chrome.browser.tab.TabLaunchType;
@@ -55,6 +56,10 @@
     public BlankCTATabInitialStateRule mBlankCTATabInitialStateRule =
             new BlankCTATabInitialStateRule(sActivityTestRule, false);
 
+    // Test activity type that does not restore tab on cold restart.
+    // Any type other than ActivityType.TABBED works.
+    private static final @ActivityType int NO_RESTORE_TYPE = ActivityType.CUSTOM_TAB;
+
     private static final String HTML_PATH =
             "/chrome/test/data/android/contextmenu/context_menu_test.html";
     private static final Pattern SCHEME_SEPARATOR_RE = Pattern.compile("://");
@@ -77,7 +82,7 @@
             super(null, tabCreatorManager, tabModelFilterFactory,
                     ()
                             -> NextTabPolicy.HIERARCHICAL,
-                    AsyncTabParamsManagerSingleton.getInstance(), false, false, false);
+                    AsyncTabParamsManagerSingleton.getInstance(), false, NO_RESTORE_TYPE, false);
         }
     }
 
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/tabmodel/TabModelSelectorObserverTestRule.java b/chrome/android/javatests/src/org/chromium/chrome/browser/tabmodel/TabModelSelectorObserverTestRule.java
index b655bedf..4cbcfbf7 100644
--- a/chrome/android/javatests/src/org/chromium/chrome/browser/tabmodel/TabModelSelectorObserverTestRule.java
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/tabmodel/TabModelSelectorObserverTestRule.java
@@ -12,6 +12,7 @@
 import org.chromium.base.CommandLine;
 import org.chromium.chrome.browser.app.tabmodel.AsyncTabParamsManagerSingleton;
 import org.chromium.chrome.browser.compositor.layouts.content.TabContentManager;
+import org.chromium.chrome.browser.flags.ActivityType;
 import org.chromium.chrome.browser.profiles.Profile;
 import org.chromium.chrome.browser.tab.Tab;
 import org.chromium.chrome.browser.tab.TabLaunchType;
@@ -28,6 +29,9 @@
  * Basis for testing tab model selector observers.
  */
 public class TabModelSelectorObserverTestRule extends ChromeBrowserTestRule {
+    // Test activity type that does not restore tab on cold restart.
+    // Any type other than ActivityType.TABBED works.
+    private static final @ActivityType int NO_RESTORE_TYPE = ActivityType.CUSTOM_TAB;
     private TabModelSelectorBase mSelector;
     private TabModelSelectorTestTabModel mNormalTabModel;
     private TabModelSelectorTestIncognitoTabModel mIncognitoTabModel;
@@ -113,7 +117,7 @@
 
         mNormalTabModel = new TabModelSelectorTestTabModel(Profile.getLastUsedRegularProfile(),
                 orderController, tabContentManager, nextTabPolicySupplier, asyncTabParamsManager,
-                delegate);
+                NO_RESTORE_TYPE, delegate);
 
         mIncognitoTabModel = new TabModelSelectorTestIncognitoTabModel(
                 Profile.getLastUsedRegularProfile().getPrimaryOTRProfile(), orderController,
@@ -131,8 +135,9 @@
         public TabModelSelectorTestTabModel(Profile profile,
                 TabModelOrderController orderController, TabContentManager tabContentManager,
                 NextTabPolicySupplier nextTabPolicySupplier,
-                AsyncTabParamsManager asyncTabParamsManager, TabModelDelegate modelDelegate) {
-            super(profile, false, null, null, orderController, tabContentManager,
+                AsyncTabParamsManager asyncTabParamsManager, @ActivityType int activityType,
+                TabModelDelegate modelDelegate) {
+            super(profile, activityType, null, null, orderController, tabContentManager,
                     nextTabPolicySupplier, asyncTabParamsManager, modelDelegate, false);
         }
 
@@ -156,14 +161,15 @@
     /**
      * Test IncognitoTabModel that exposes the needed capabilities for testing.
      */
-    public static class TabModelSelectorTestIncognitoTabModel
+    private static class TabModelSelectorTestIncognitoTabModel
             extends TabModelSelectorTestTabModel implements IncognitoTabModel {
         public TabModelSelectorTestIncognitoTabModel(Profile profile,
                 TabModelOrderController orderController, TabContentManager tabContentManager,
                 NextTabPolicySupplier nextTabPolicySupplier,
                 AsyncTabParamsManager asyncTabParamsManager, TabModelDelegate modelDelegate) {
             super(Profile.getLastUsedRegularProfile().getPrimaryOTRProfile(), orderController,
-                    tabContentManager, nextTabPolicySupplier, asyncTabParamsManager, modelDelegate);
+                    tabContentManager, nextTabPolicySupplier, asyncTabParamsManager,
+                    NO_RESTORE_TYPE, modelDelegate);
         }
 
         @Override
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/tabmodel/TabPersistentStoreTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/tabmodel/TabPersistentStoreTest.java
index 164506d..442f3c9d 100644
--- a/chrome/android/javatests/src/org/chromium/chrome/browser/tabmodel/TabPersistentStoreTest.java
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/tabmodel/TabPersistentStoreTest.java
@@ -87,6 +87,10 @@
 @CommandLineFlags.
 Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE, "force-fieldtrials=Study/Group"})
 public class TabPersistentStoreTest {
+    // Test activity type that does not restore tab on cold restart.
+    // Any type other than ActivityType.TABBED works.
+    private static final @ActivityType int NO_RESTORE_TYPE = ActivityType.CUSTOM_TAB;
+
     @Rule
     public final ChromeBrowserTestRule mBrowserTestRule = new ChromeBrowserTestRule();
 
@@ -149,7 +153,7 @@
             Callable<TabModelImpl> callable = new Callable<TabModelImpl>() {
                 @Override
                 public TabModelImpl call() {
-                    return new TabModelImpl(Profile.getLastUsedRegularProfile(), false,
+                    return new TabModelImpl(Profile.getLastUsedRegularProfile(), NO_RESTORE_TYPE,
                             getTabCreatorManager().getTabCreator(false),
                             getTabCreatorManager().getTabCreator(true), mTabModelOrderController,
                             null, nextTabPolicySupplier,
@@ -163,7 +167,7 @@
                             getTabCreatorManager().getTabCreator(false),
                             getTabCreatorManager().getTabCreator(true), mTabModelOrderController,
                             null, nextTabPolicySupplier,
-                            AsyncTabParamsManagerSingleton.getInstance(), this));
+                            AsyncTabParamsManagerSingleton.getInstance(), NO_RESTORE_TYPE, this));
             initialize(regularTabModel, incognitoTabModel);
         }
 
diff --git a/chrome/android/junit/src/org/chromium/chrome/browser/tabmodel/TabModelSelectorImplTest.java b/chrome/android/junit/src/org/chromium/chrome/browser/tabmodel/TabModelSelectorImplTest.java
index 9b86933..79db6842 100644
--- a/chrome/android/junit/src/org/chromium/chrome/browser/tabmodel/TabModelSelectorImplTest.java
+++ b/chrome/android/junit/src/org/chromium/chrome/browser/tabmodel/TabModelSelectorImplTest.java
@@ -19,6 +19,7 @@
 
 import org.chromium.base.test.BaseRobolectricTestRunner;
 import org.chromium.chrome.browser.compositor.layouts.content.TabContentManager;
+import org.chromium.chrome.browser.flags.ActivityType;
 import org.chromium.chrome.browser.tab.MockTab;
 import org.chromium.chrome.browser.tab.TabCreationState;
 import org.chromium.chrome.browser.tab.TabDelegateFactory;
@@ -34,6 +35,10 @@
 @RunWith(BaseRobolectricTestRunner.class)
 @Config(manifest = Config.NONE)
 public class TabModelSelectorImplTest {
+    // Test activity type that does not restore tab on cold restart.
+    // Any type other than ActivityType.TABBED works.
+    private static final @ActivityType int NO_RESTORE_TYPE = ActivityType.CUSTOM_TAB;
+
     @Mock
     TabModelFilterFactory mMockTabModelFilterFactory;
     @Mock
@@ -54,12 +59,12 @@
                 .when(mMockTabModelFilterFactory)
                 .createTabModelFilter(any());
         mTabCreatorManager = new MockTabCreatorManager();
+
         AsyncTabParamsManager realAsyncTabParamsManager =
                 AsyncTabParamsManagerFactory.createAsyncTabParamsManager();
         mTabModelSelector = new TabModelSelectorImpl(null, mTabCreatorManager,
                 mMockTabModelFilterFactory, mNextTabPolicySupplier, realAsyncTabParamsManager,
-                /*supportUndo=*/false,
-                /*isTabbedActivity=*/false, /*startIncognito=*/false);
+                /*supportUndo=*/false, NO_RESTORE_TYPE, /*startIncognito=*/false);
         mTabCreatorManager.initialize(mTabModelSelector);
         mTabModelSelector.onNativeLibraryReadyInternal(mMockTabContentManager,
                 new MockTabModel(false, null), new MockTabModel(true, null));
@@ -99,4 +104,4 @@
         Assert.assertEquals("tab shouldn't be removed while reparenting is in progress", 1,
                 mTabModelSelector.getModel(false).getCount());
     }
-}
\ No newline at end of file
+}
diff --git a/chrome/android/profiles/newest.txt b/chrome/android/profiles/newest.txt
index 532cc84..2bac2fb 100644
--- a/chrome/android/profiles/newest.txt
+++ b/chrome/android/profiles/newest.txt
@@ -1 +1 @@
-chromeos-chrome-amd64-91.0.4456.0_rc-r1-merged.afdo.bz2
+chromeos-chrome-amd64-91.0.4459.0_rc-r1-merged.afdo.bz2
diff --git a/chrome/browser/about_flags.cc b/chrome/browser/about_flags.cc
index d6b9de8d..4e84b54 100644
--- a/chrome/browser/about_flags.cc
+++ b/chrome/browser/about_flags.cc
@@ -6222,6 +6222,9 @@
 #endif
 
 #if BUILDFLAG(IS_CHROMEOS_ASH)
+    {"crosh-swa", flag_descriptions::kCroshSWAName,
+     flag_descriptions::kCroshSWADescription, kOsCrOS,
+     FEATURE_VALUE_TYPE(chromeos::features::kCroshSWA)},
     {"crostini-use-buster-image",
      flag_descriptions::kCrostiniUseBusterImageName,
      flag_descriptions::kCrostiniUseBusterImageDescription, kOsCrOS,
diff --git a/chrome/browser/ash/web_applications/terminal_source.cc b/chrome/browser/ash/web_applications/terminal_source.cc
index aae111c..77fdf33 100644
--- a/chrome/browser/ash/web_applications/terminal_source.cc
+++ b/chrome/browser/ash/web_applications/terminal_source.cc
@@ -4,7 +4,9 @@
 
 #include "chrome/browser/ash/web_applications/terminal_source.h"
 
+#include "ash/constants/ash_features.h"
 #include "base/containers/flat_map.h"
+#include "base/feature_list.h"
 #include "base/files/file_path.h"
 #include "base/files/file_util.h"
 #include "base/memory/ptr_util.h"
@@ -49,6 +51,7 @@
   if (!result) {
     static const base::NoDestructor<base::flat_map<std::string, std::string>>
         kTestFiles({
+            {"html/crosh.html", ""},
             {"html/terminal.html", "<script src='/js/terminal.js'></script>"},
             {"js/terminal.js",
              "chrome.terminalPrivate.openVmshellProcess([], () => {})"},
@@ -70,18 +73,24 @@
 
 // static
 std::unique_ptr<TerminalSource> TerminalSource::ForCrosh(Profile* profile) {
-  return base::WrapUnique(
-      new TerminalSource(profile, chrome::kChromeUIUntrustedCroshURL));
+  std::string default_file = "html/crosh.html";
+  if (base::FeatureList::IsEnabled(chromeos::features::kCroshSWA)) {
+    default_file = "html/terminal.html";
+  }
+  return base::WrapUnique(new TerminalSource(
+      profile, chrome::kChromeUIUntrustedCroshURL, default_file));
 }
 
 // static
 std::unique_ptr<TerminalSource> TerminalSource::ForTerminal(Profile* profile) {
-  return base::WrapUnique(
-      new TerminalSource(profile, chrome::kChromeUIUntrustedTerminalURL));
+  return base::WrapUnique(new TerminalSource(
+      profile, chrome::kChromeUIUntrustedTerminalURL, "html/terminal.html"));
 }
 
-TerminalSource::TerminalSource(Profile* profile, std::string source)
-    : profile_(profile), source_(source) {
+TerminalSource::TerminalSource(Profile* profile,
+                               std::string source,
+                               std::string default_file)
+    : profile_(profile), source_(source), default_file_(default_file) {
   auto* webui_allowlist = WebUIAllowlist::GetOrCreate(profile);
   const url::Origin terminal_origin = url::Origin::Create(GURL(source));
   CHECK(!terminal_origin.opaque());
@@ -112,7 +121,7 @@
   // skip first '/' in path.
   std::string path = url.path().substr(1);
   if (path.empty())
-    path = "html/terminal.html";
+    path = default_file_;
 
   // Replace $i8n{themeColor} in *.html.
   if (base::EndsWith(path, ".html", base::CompareCase::INSENSITIVE_ASCII)) {
diff --git a/chrome/browser/ash/web_applications/terminal_source.h b/chrome/browser/ash/web_applications/terminal_source.h
index 25ed809..fc4acc5 100644
--- a/chrome/browser/ash/web_applications/terminal_source.h
+++ b/chrome/browser/ash/web_applications/terminal_source.h
@@ -18,6 +18,9 @@
 
 class Profile;
 
+// Provides the web (html / js / css) content for crostini Terminal and crosh.
+// This content is provided by chromiumos in the rootfs at
+// /usr/share/chromeos-assets/crosh_builtin.
 class TerminalSource : public content::URLDataSource {
  public:
   static std::unique_ptr<TerminalSource> ForCrosh(Profile* profile);
@@ -27,7 +30,9 @@
   ~TerminalSource() override;
 
  private:
-  explicit TerminalSource(Profile* profile, std::string source);
+  TerminalSource(Profile* profile,
+                 std::string source,
+                 std::string default_file);
 
   // content::URLDataSource:
   std::string GetSource() override;
@@ -46,6 +51,7 @@
 
   Profile* profile_;
   std::string source_;
+  std::string default_file_;
   ui::TemplateReplacements replacements_;
 
   DISALLOW_COPY_AND_ASSIGN(TerminalSource);
diff --git a/chrome/browser/autocomplete/chrome_autocomplete_provider_client.cc b/chrome/browser/autocomplete/chrome_autocomplete_provider_client.cc
index 5f889f5..931946d 100644
--- a/chrome/browser/autocomplete/chrome_autocomplete_provider_client.cc
+++ b/chrome/browser/autocomplete/chrome_autocomplete_provider_client.cc
@@ -63,6 +63,7 @@
 #include "chrome/android/chrome_jni_headers/ChromeAutocompleteProviderClient_jni.h"
 #include "chrome/browser/android/tab_android.h"
 #include "chrome/browser/android/tab_android_user_data.h"
+#include "chrome/browser/flags/android/chrome_session_state.h"
 #include "chrome/browser/ui/android/tab_model/tab_model.h"
 #include "chrome/browser/ui/android/tab_model/tab_model_jni_bridge.h"
 #include "chrome/browser/ui/android/tab_model/tab_model_list.h"
@@ -73,6 +74,10 @@
 #include "chrome/browser/upgrade_detector/upgrade_detector.h"
 #endif
 
+#if defined(OS_ANDROID)
+using chrome::android::ActivityType;
+#endif
+
 namespace {
 
 #if defined(OS_ANDROID)
@@ -577,15 +582,20 @@
   jclass tab_model_clazz = TabModelJniBridge::GetClazz(env);
   base::android::ScopedJavaLocalRef<jobjectArray> j_tab_model_array(
       env, env->NewObjectArray(tab_models.size(), tab_model_clazz, nullptr));
+  // Get all the hidden and non CCT tabs. Filter the tabs in CCT tabmodel first.
   for (size_t i = 0; i < tab_models.size(); ++i) {
+    ActivityType type = tab_models[i]->activity_type();
+    if (type == ActivityType::kCustomTab ||
+        type == ActivityType::kTrustedWebActivity) {
+      continue;
+    }
     env->SetObjectArrayElement(j_tab_model_array.obj(), i,
                                tab_models[i]->GetJavaObject().obj());
   }
 
-  // Get all the hidden and non CCT tabs.
   base::android::ScopedJavaLocalRef<jobjectArray> j_tabs =
-      Java_ChromeAutocompleteProviderClient_getAllHiddenAndNonCCTTabs(
-          env, j_tab_model_array);
+      Java_ChromeAutocompleteProviderClient_getAllHiddenTabs(env,
+                                                             j_tab_model_array);
   if (j_tabs.is_null())
     return std::vector<TabAndroid*>();
 
diff --git a/chrome/browser/continuous_search/android/java/src/org/chromium/chrome/browser/continuous_search/ContinuousSearchContainerMediator.java b/chrome/browser/continuous_search/android/java/src/org/chromium/chrome/browser/continuous_search/ContinuousSearchContainerMediator.java
index e3b185b..6d82491 100644
--- a/chrome/browser/continuous_search/android/java/src/org/chromium/chrome/browser/continuous_search/ContinuousSearchContainerMediator.java
+++ b/chrome/browser/continuous_search/android/java/src/org/chromium/chrome/browser/continuous_search/ContinuousSearchContainerMediator.java
@@ -53,6 +53,8 @@
      */
     void updateTabObscured(boolean isObscured) {
         mIsTabObscured = isObscured;
+        if (mModel == null) return;
+
         mModel.set(ContinuousSearchContainerProperties.ANDROID_VIEW_VISIBILITY,
                 !mIsTabObscured && mIsVisible ? View.VISIBLE : View.INVISIBLE);
     }
diff --git a/chrome/browser/flag-metadata.json b/chrome/browser/flag-metadata.json
index 8d64b30..3613208 100644
--- a/chrome/browser/flag-metadata.json
+++ b/chrome/browser/flag-metadata.json
@@ -776,6 +776,11 @@
     "expiry_milestone": 80
   },
   {
+    "name": "crosh-swa",
+    "owners": [ "joelhockey", "benwells" ],
+    "expiry_milestone": 93
+  },
+  {
     "name": "cross-origin-isolated",
     "owners": [ "ahemery", "arthursonzogni", "clamy", "pmeuleman" ],
     "expiry_milestone": 88
diff --git a/chrome/browser/flag_descriptions.cc b/chrome/browser/flag_descriptions.cc
index 30b8fb8..e3fe615 100644
--- a/chrome/browser/flag_descriptions.cc
+++ b/chrome/browser/flag_descriptions.cc
@@ -3905,6 +3905,11 @@
     "Enables contextual nudges, periodically showing the user a label "
     "explaining how to interact with a particular UI element using gestures.";
 
+const char kCroshSWAName[] = "Crosh System Web App";
+const char kCroshSWADescription[] =
+    "When enabled, crosh (Chrome OS Shell) will run as a tabbed System Web App "
+    "rather than a normal browser tab.";
+
 const char kCrosLanguageSettingsUpdateName[] = "Language Settings Update";
 const char kCrosLanguageSettingsUpdateDescription[] =
     "Enable this flag to see the new language settings update.";
diff --git a/chrome/browser/flag_descriptions.h b/chrome/browser/flag_descriptions.h
index 8490126a..e9cfc8f 100644
--- a/chrome/browser/flag_descriptions.h
+++ b/chrome/browser/flag_descriptions.h
@@ -2246,6 +2246,9 @@
 extern const char kContextualNudgesName[];
 extern const char kContextualNudgesDescription[];
 
+extern const char kCroshSWAName[];
+extern const char kCroshSWADescription[];
+
 extern const char kCrosLanguageSettingsUpdateName[];
 extern const char kCrosLanguageSettingsUpdateDescription[];
 
diff --git a/chrome/browser/metrics/ukm_browsertest.cc b/chrome/browser/metrics/ukm_browsertest.cc
index 38e117a..a2c2db6d 100644
--- a/chrome/browser/metrics/ukm_browsertest.cc
+++ b/chrome/browser/metrics/ukm_browsertest.cc
@@ -69,6 +69,7 @@
 #include "chrome/test/base/in_process_browser_test.h"
 #include "chrome/test/base/ui_test_utils.h"
 #else
+#include "chrome/browser/flags/android/chrome_session_state.h"
 #include "chrome/browser/ui/android/tab_model/tab_model.h"
 #include "chrome/browser/ui/android/tab_model/tab_model_list.h"
 #include "chrome/browser/ui/android/tab_model/tab_model_observer.h"
@@ -106,12 +107,17 @@
 }
 
 #if defined(OS_ANDROID)
+
+// ActivityType that doesn't restore tabs on cold start.
+// Any type other than kTabbed is fine.
+const auto TEST_ACTIVITY_TYPE = chrome::android::ActivityType::kCustomTab;
+
 // TestTabModel provides a means of creating a tab associated with a given
 // profile. The new tab can then be added to Android's TabModelList.
 class TestTabModel : public TabModel {
  public:
   explicit TestTabModel(Profile* profile)
-      : TabModel(profile, /*is_tabbed_activity=*/false),
+      : TabModel(profile, TEST_ACTIVITY_TYPE),
         web_contents_(content::WebContents::Create(
             content::WebContents::CreateParams(GetProfile()))) {}
 
diff --git a/chrome/browser/optimization_guide/android/optimization_guide_tab_url_provider_android_unittest.cc b/chrome/browser/optimization_guide/android/optimization_guide_tab_url_provider_android_unittest.cc
index 5a4d9ac..481e6e94 100644
--- a/chrome/browser/optimization_guide/android/optimization_guide_tab_url_provider_android_unittest.cc
+++ b/chrome/browser/optimization_guide/android/optimization_guide_tab_url_provider_android_unittest.cc
@@ -24,7 +24,7 @@
   explicit FakeTabModel(
       Profile* profile,
       const std::vector<content::WebContents*>& web_contents_list)
-      : TabModel(profile, /*is_tabbed_activity=*/false),
+      : TabModel(profile, chrome::android::ActivityType::kCustomTab),
         web_contents_list_(web_contents_list) {}
 
   int GetTabCount() const override {
diff --git a/chrome/browser/policy/configuration_policy_handler_list_factory.cc b/chrome/browser/policy/configuration_policy_handler_list_factory.cc
index 3d8226b4..dcd5b3d 100644
--- a/chrome/browser/policy/configuration_policy_handler_list_factory.cc
+++ b/chrome/browser/policy/configuration_policy_handler_list_factory.cc
@@ -454,6 +454,9 @@
   { key::kDisableSafeBrowsingProceedAnyway,
     prefs::kSafeBrowsingProceedAnywayDisabled,
     base::Value::Type::BOOLEAN },
+  { key::kCECPQ2Enabled,
+    prefs::kCECPQ2Enabled,
+    base::Value::Type::BOOLEAN },
   { key::kSSLErrorOverrideAllowed,
     prefs::kSSLErrorOverrideAllowed,
     base::Value::Type::BOOLEAN },
diff --git a/chrome/browser/policy/policy_network_browsertest.cc b/chrome/browser/policy/policy_network_browsertest.cc
index b08887c..a0bf4d84 100644
--- a/chrome/browser/policy/policy_network_browsertest.cc
+++ b/chrome/browser/policy/policy_network_browsertest.cc
@@ -9,11 +9,14 @@
 #include "base/memory/ref_counted.h"
 #include "base/run_loop.h"
 #include "base/strings/string_number_conversions.h"
+#include "base/strings/utf_string_conversions.h"
+#include "base/test/scoped_feature_list.h"
 #include "base/threading/thread_restrictions.h"
 #include "build/build_config.h"
 #include "build/chromeos_buildflags.h"
 #include "chrome/browser/browser_process.h"
 #include "chrome/browser/net/system_network_context_manager.h"
+#include "chrome/browser/policy/policy_test_utils.h"
 #include "chrome/browser/policy/profile_policy_connector_builder.h"
 #include "chrome/browser/profiles/profile.h"
 #include "chrome/browser/profiles/profile_manager.h"
@@ -21,27 +24,34 @@
 #include "chrome/browser/safe_browsing/safe_browsing_service.h"
 #include "chrome/browser/ui/browser.h"
 #include "chrome/common/chrome_switches.h"
+#include "chrome/common/pref_names.h"
 #include "chrome/test/base/in_process_browser_test.h"
+#include "chrome/test/base/testing_browser_process.h"
+#include "chrome/test/base/ui_test_utils.h"
 #include "components/network_session_configurator/common/network_switches.h"
 #include "components/policy/core/browser/browser_policy_connector.h"
 #include "components/policy/core/common/mock_configuration_policy_provider.h"
 #include "components/policy/core/common/policy_map.h"
 #include "components/policy/core/common/policy_types.h"
 #include "components/policy/policy_constants.h"
+#include "components/prefs/pref_service.h"
 #include "content/public/browser/browser_context.h"
 #include "content/public/browser/browser_thread.h"
+#include "content/public/browser/navigation_entry.h"
 #include "content/public/browser/network_service_instance.h"
 #include "content/public/browser/storage_partition.h"
 #include "content/public/common/network_service_util.h"
 #include "content/public/test/browser_test.h"
 #include "content/public/test/browser_test_utils.h"
 #include "content/public/test/content_mock_cert_verifier.h"
+#include "net/base/features.h"
 #include "net/dns/mock_host_resolver.h"
 #include "net/test/cert_test_util.h"
 #include "net/test/quic_simple_test_server.h"
 #include "net/test/test_data_directory.h"
 #include "services/network/public/cpp/features.h"
 #include "services/network/public/mojom/network_service_test.mojom.h"
+#include "third_party/boringssl/src/include/openssl/obj.h"
 
 #if BUILDFLAG(IS_CHROMEOS_ASH)
 #include "ash/constants/ash_switches.h"
@@ -572,4 +582,79 @@
   EXPECT_FALSE(IsQuicEnabled(profile_2()));
 }
 
+class CECPQ2PolicyTest : public PolicyTest {
+ public:
+  CECPQ2PolicyTest() {
+    scoped_feature_list_.InitAndEnableFeature(
+        net::features::kPostQuantumCECPQ2);
+  }
+
+ protected:
+  // RunTest checks that CECPQ2 works initially but stops working after
+  // |update_policy| has run.
+  void RunTest(base::OnceCallback<void(PolicyMap*)> update_policy) {
+    // A test server is configured with support for only CECPQ2.
+    net::EmbeddedTestServer https_server_ok(
+        net::EmbeddedTestServer::TYPE_HTTPS);
+    net::SSLServerConfig ssl_config;
+    ssl_config.curves_for_testing = {NID_CECPQ2};
+    https_server_ok.SetSSLConfig(net::EmbeddedTestServer::CERT_OK, ssl_config);
+    https_server_ok.ServeFilesFromSourceDirectory("chrome/test/data");
+    ASSERT_TRUE(https_server_ok.Start());
+
+    PrefService* const prefs = g_browser_process->local_state();
+    EXPECT_TRUE(prefs->GetBoolean(prefs::kCECPQ2Enabled));
+
+    // Should be able to load a page from the test server because CECPQ2 is
+    // enabled.
+    ui_test_utils::NavigateToURL(browser(),
+                                 https_server_ok.GetURL("/title2.html"));
+    content::WebContents* web_contents =
+        browser()->tab_strip_model()->GetActiveWebContents();
+    EXPECT_EQ(base::UTF8ToUTF16("Title Of Awesomeness"),
+              web_contents->GetTitle());
+    EXPECT_NE(
+        web_contents->GetController().GetLastCommittedEntry()->GetPageType(),
+        content::PAGE_TYPE_ERROR);
+
+    PolicyMap policies;
+    std::move(update_policy).Run(&policies);
+    UpdateProviderPolicy(policies);
+    content::FlushNetworkServiceInstanceForTesting();
+
+    // Page loads should now fail.
+    const GURL fail_url = https_server_ok.GetURL("/title3.html");
+    ui_test_utils::NavigateToURL(browser(), fail_url);
+    web_contents = browser()->tab_strip_model()->GetActiveWebContents();
+    EXPECT_EQ(
+        web_contents->GetController().GetLastCommittedEntry()->GetPageType(),
+        content::PAGE_TYPE_ERROR);
+  }
+
+  base::test::ScopedFeatureList scoped_feature_list_;
+};
+
+IN_PROC_BROWSER_TEST_F(CECPQ2PolicyTest, CECPQ2EnabledPolicy) {
+  RunTest(base::BindOnce([](PolicyMap* policies) {
+    SetPolicy(policies, key::kCECPQ2Enabled, base::Value(false));
+  }));
+}
+
+IN_PROC_BROWSER_TEST_F(CECPQ2PolicyTest, ChromeVariations) {
+  // Setting ChromeVariations to a non-zero value should also disable
+  // CECPQ2.
+  RunTest(base::BindOnce([](PolicyMap* policies) {
+    const auto* const variations_key =
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+        // On Chrome OS the ChromeVariations policy doesn't exist and is
+        // replaced by DeviceChromeVariations.
+        key::kDeviceChromeVariations;
+#else
+        key::kChromeVariations;
+#endif
+
+    SetPolicy(policies, variations_key, base::Value(1));
+  }));
+}
+
 }  // namespace policy
diff --git a/chrome/browser/ssl/ssl_config_service_manager_pref.cc b/chrome/browser/ssl/ssl_config_service_manager_pref.cc
index dc0bd94..be9fab3 100644
--- a/chrome/browser/ssl/ssl_config_service_manager_pref.cc
+++ b/chrome/browser/ssl/ssl_config_service_manager_pref.cc
@@ -15,6 +15,7 @@
 #include "base/macros.h"
 #include "base/strings/string_util.h"
 #include "base/values.h"
+#include "build/build_config.h"
 #include "chrome/common/chrome_switches.h"
 #include "chrome/common/pref_names.h"
 #include "components/content_settings/core/browser/content_settings_utils.h"
@@ -23,6 +24,8 @@
 #include "components/prefs/pref_member.h"
 #include "components/prefs/pref_registry_simple.h"
 #include "components/prefs/pref_service.h"
+#include "components/variations/client_filterable_state.h"
+#include "components/variations/pref_names.h"
 #include "mojo/public/cpp/bindings/remote.h"
 #include "mojo/public/cpp/bindings/remote_set.h"
 #include "net/cert/cert_verifier.h"
@@ -36,6 +39,15 @@
 
 namespace {
 
+const char* kVariationsRestrictionsByPolicy =
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+    // On Chrome OS the ChromeVariations policy doesn't exist and is replaced by
+    // DeviceChromeVariations.
+    variations::prefs::kDeviceVariationsRestrictionsByPolicy;
+#else
+    variations::prefs::kVariationsRestrictionsByPolicy;
+#endif
+
 // Converts a ListValue of StringValues into a vector of strings. Any Values
 // which cannot be converted will be skipped.
 std::vector<std::string> ListValueToStringVector(const base::ListValue* value) {
@@ -138,6 +150,18 @@
   // cached list of parsed SSL/TLS cipher suites that are disabled.
   void OnDisabledCipherSuitesChange(PrefService* local_state);
 
+  void CacheVariationsPolicy(PrefService* local_state) {
+    const PrefService::Preference* const pref =
+        local_state->FindPreference(kVariationsRestrictionsByPolicy);
+    // kVariationsRestrictionsByPolicy may not be registered in test contexts
+    // therefore that case is handled by assuming that it has the default value
+    // of |NO_RESTRICTIONS|.
+    variations_unrestricted_ =
+        !pref ||
+        pref->GetValue()->GetInt() ==
+            static_cast<int>(variations::RestrictionPolicy::NO_RESTRICTIONS);
+  }
+
   PrefChangeRegistrar local_state_change_registrar_;
 
   // The local_state prefs.
@@ -146,10 +170,15 @@
   StringPrefMember ssl_version_min_;
   StringPrefMember ssl_version_max_;
   StringListPrefMember h2_client_cert_coalescing_host_patterns_;
+  BooleanPrefMember cecpq2_enabled_;
 
   // The cached list of disabled SSL cipher suites.
   std::vector<uint16_t> disabled_cipher_suites_;
 
+  // variations_unrestricted_ is true iff the ChromeVariations policy has not
+  // been set to anything more restrictive than the default NO_RESTRICTIONS.
+  bool variations_unrestricted_ = true;
+
   mojo::RemoteSet<network::mojom::SSLConfigClient> ssl_config_client_set_;
 
   DISALLOW_COPY_AND_ASSIGN(SSLConfigServiceManagerPref);
@@ -174,13 +203,19 @@
                         local_state_callback);
   h2_client_cert_coalescing_host_patterns_.Init(
       prefs::kH2ClientCertCoalescingHosts, local_state, local_state_callback);
+  cecpq2_enabled_.Init(prefs::kCECPQ2Enabled, local_state,
+                       local_state_callback);
 
   local_state_change_registrar_.Init(local_state);
   local_state_change_registrar_.Add(prefs::kCipherSuiteBlacklist,
                                     local_state_callback);
+  local_state_change_registrar_.Add(kVariationsRestrictionsByPolicy,
+                                    local_state_callback);
 
   // Populate |disabled_cipher_suites_| with the initial pref value.
   OnDisabledCipherSuitesChange(local_state);
+
+  CacheVariationsPolicy(local_state);
 }
 
 // static
@@ -196,6 +231,8 @@
   registry->RegisterStringPref(prefs::kSSLVersionMax, std::string());
   registry->RegisterListPref(prefs::kCipherSuiteBlacklist);
   registry->RegisterListPref(prefs::kH2ClientCertCoalescingHosts);
+  registry->RegisterBooleanPref(prefs::kCECPQ2Enabled,
+                                default_context_config.cecpq2_enabled);
 }
 
 void SSLConfigServiceManagerPref::AddToNetworkContextParams(
@@ -218,6 +255,8 @@
   if (pref_name_in == prefs::kCipherSuiteBlacklist)
     OnDisabledCipherSuitesChange(prefs);
 
+  CacheVariationsPolicy(prefs);
+
   network::mojom::SSLConfigPtr new_config = GetSSLConfigFromPrefs();
   network::mojom::SSLConfig* raw_config = new_config.get();
 
@@ -259,6 +298,11 @@
   config->disabled_cipher_suites = disabled_cipher_suites_;
   config->client_cert_pooling_policy = CanonicalizeHostnamePatterns(
       h2_client_cert_coalescing_host_patterns_.GetValue());
+  // CECPQ2 is not enabled if ChromeVariations has been set to limit the
+  // applicability of Finch trials. We take that as a signal that the customer
+  // is especially conservative.
+  config->cecpq2_enabled =
+      cecpq2_enabled_.GetValue() && variations_unrestricted_;
 
   return config;
 }
diff --git a/chrome/browser/ui/BUILD.gn b/chrome/browser/ui/BUILD.gn
index ef01360..3a0fef4 100644
--- a/chrome/browser/ui/BUILD.gn
+++ b/chrome/browser/ui/BUILD.gn
@@ -824,6 +824,7 @@
     deps += [
       "//chrome/android:jni_headers",
       "//chrome/android/features/dev_ui:buildflags",
+      "//chrome/browser/flags:flags_android",
       "//chrome/browser/image_decoder",
       "//chrome/browser/resources/webapks:resources",
       "//components/autofill_assistant/browser",
diff --git a/chrome/browser/ui/android/tab_model/tab_model.cc b/chrome/browser/ui/android/tab_model/tab_model.cc
index 94d982c..25d4bc8 100644
--- a/chrome/browser/ui/android/tab_model/tab_model.cc
+++ b/chrome/browser/ui/android/tab_model/tab_model.cc
@@ -11,16 +11,19 @@
 #include "chrome/browser/sync/sessions/sync_sessions_web_contents_router_factory.h"
 #include "components/omnibox/browser/location_bar_model_impl.h"
 
+using chrome::android::ActivityType;
+
 // Keep this in sync with
 // chrome/android/java/src/org/chromium/chrome/browser/tabmodel/TabList.java
 static int INVALID_TAB_INDEX = -1;
 
-TabModel::TabModel(Profile* profile, bool is_tabbed_activity)
+TabModel::TabModel(Profile* profile, ActivityType activity_type)
     : profile_(profile),
+      activity_type_(activity_type),
       live_tab_context_(new AndroidLiveTabContext(this)),
-      synced_window_delegate_(
-          new browser_sync::SyncedWindowDelegateAndroid(this,
-                                                        is_tabbed_activity)),
+      synced_window_delegate_(new browser_sync::SyncedWindowDelegateAndroid(
+          this,
+          activity_type == ActivityType::kTabbed)),
       session_id_(SessionID::NewUnique()) {}
 
 TabModel::~TabModel() = default;
diff --git a/chrome/browser/ui/android/tab_model/tab_model.h b/chrome/browser/ui/android/tab_model/tab_model.h
index 790582f..85e2197 100644
--- a/chrome/browser/ui/android/tab_model/tab_model.h
+++ b/chrome/browser/ui/android/tab_model/tab_model.h
@@ -9,6 +9,7 @@
 
 #include "base/android/scoped_java_ref.h"
 #include "base/macros.h"
+#include "chrome/browser/flags/android/chrome_session_state.h"
 #include "chrome/browser/ui/android/tab_model/android_live_tab_context.h"
 #include "components/omnibox/browser/location_bar_model.h"
 #include "components/omnibox/browser/location_bar_model_delegate.h"
@@ -150,8 +151,10 @@
   // Removes an observer from this TabModel.
   virtual void RemoveObserver(TabModelObserver* observer) = 0;
 
+  chrome::android::ActivityType activity_type() const { return activity_type_; }
+
  protected:
-  explicit TabModel(Profile* profile, bool is_tabbed_activity);
+  TabModel(Profile* profile, chrome::android::ActivityType activity_type);
   virtual ~TabModel();
 
   // Instructs the TabModel to broadcast a notification that all tabs are now
@@ -163,6 +166,8 @@
  private:
   Profile* profile_;
 
+  chrome::android::ActivityType activity_type_;
+
   // The LiveTabContext associated with TabModel.
   // Used to restore closed tabs through the TabRestoreService.
   std::unique_ptr<AndroidLiveTabContext> live_tab_context_;
diff --git a/chrome/browser/ui/android/tab_model/tab_model_jni_bridge.cc b/chrome/browser/ui/android/tab_model/tab_model_jni_bridge.cc
index 2c9b074..d0d9ebf 100644
--- a/chrome/browser/ui/android/tab_model/tab_model_jni_bridge.cc
+++ b/chrome/browser/ui/android/tab_model/tab_model_jni_bridge.cc
@@ -30,13 +30,14 @@
 using base::android::ConvertUTF8ToJavaString;
 using base::android::JavaParamRef;
 using base::android::ScopedJavaLocalRef;
+using chrome::android::ActivityType;
 using content::WebContents;
 
 TabModelJniBridge::TabModelJniBridge(JNIEnv* env,
                                      jobject jobj,
                                      Profile* profile,
-                                     bool is_tabbed_activity)
-    : TabModel(profile, is_tabbed_activity),
+                                     ActivityType activity_type)
+    : TabModel(profile, activity_type),
       java_object_(env, env->NewWeakGlobalRef(jobj)) {
   TabModelList::AddTabModel(this);
 }
@@ -230,9 +231,9 @@
 static jlong JNI_TabModelJniBridge_Init(JNIEnv* env,
                                         const JavaParamRef<jobject>& obj,
                                         const JavaParamRef<jobject>& j_profile,
-                                        jboolean is_tabbed_activity) {
+                                        jint j_activity_type) {
   TabModel* tab_model = new TabModelJniBridge(
       env, obj, ProfileAndroid::FromProfileAndroid(j_profile),
-      is_tabbed_activity);
+      static_cast<ActivityType>(j_activity_type));
   return reinterpret_cast<intptr_t>(tab_model);
 }
diff --git a/chrome/browser/ui/android/tab_model/tab_model_jni_bridge.h b/chrome/browser/ui/android/tab_model/tab_model_jni_bridge.h
index 65cef11..8050a94 100644
--- a/chrome/browser/ui/android/tab_model/tab_model_jni_bridge.h
+++ b/chrome/browser/ui/android/tab_model/tab_model_jni_bridge.h
@@ -12,6 +12,7 @@
 #include "base/android/scoped_java_ref.h"
 #include "base/compiler_specific.h"
 #include "base/macros.h"
+#include "chrome/browser/flags/android/chrome_session_state.h"
 #include "chrome/browser/ui/android/tab_model/tab_model.h"
 
 class TabAndroid;
@@ -29,7 +30,7 @@
   TabModelJniBridge(JNIEnv* env,
                     jobject obj,
                     Profile* profile,
-                    bool is_tabbed_activity);
+                    chrome::android::ActivityType activity_type);
   void Destroy(JNIEnv* env, const base::android::JavaParamRef<jobject>& obj);
   ~TabModelJniBridge() override;
 
diff --git a/chrome/browser/ui/android/tab_model/tab_model_list_unittest.cc b/chrome/browser/ui/android/tab_model/tab_model_list_unittest.cc
index 3f1f7b6d..cc7f78a 100644
--- a/chrome/browser/ui/android/tab_model/tab_model_list_unittest.cc
+++ b/chrome/browser/ui/android/tab_model/tab_model_list_unittest.cc
@@ -2,8 +2,9 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-#include "chrome/browser/ui/android/tab_model/tab_model.h"
 #include "chrome/browser/ui/android/tab_model/tab_model_list.h"
+#include "chrome/browser/flags/android/chrome_session_state.h"
+#include "chrome/browser/ui/android/tab_model/tab_model.h"
 #include "chrome/test/base/chrome_render_view_host_test_harness.h"
 #include "chrome/test/base/testing_profile.h"
 #include "content/public/browser/web_contents.h"
@@ -15,7 +16,8 @@
 class TestTabModel : public TabModel {
  public:
   explicit TestTabModel(Profile* profile)
-      : TabModel(profile, false), tab_count_(0) {}
+      : TabModel(profile, chrome::android::ActivityType::kCustomTab),
+        tab_count_(0) {}
 
   int GetTabCount() const override { return tab_count_; }
   int GetActiveIndex() const override { return 0; }
diff --git a/chrome/browser/ui/ash/chrome_new_window_client.cc b/chrome/browser/ui/ash/chrome_new_window_client.cc
index 956783b..38dd0f2 100644
--- a/chrome/browser/ui/ash/chrome_new_window_client.cc
+++ b/chrome/browser/ui/ash/chrome_new_window_client.cc
@@ -417,7 +417,19 @@
 
 void ChromeNewWindowClient::OpenCrosh() {
   Profile* profile = ProfileManager::GetActiveUserProfile();
-  web_app::LaunchSystemWebAppAsync(profile, web_app::SystemAppType::CROSH);
+  if (base::FeatureList::IsEnabled(chromeos::features::kCroshSWA)) {
+    web_app::LaunchSystemWebAppAsync(profile, web_app::SystemAppType::CROSH);
+  } else {
+    chrome::ScopedTabbedBrowserDisplayer displayer(profile);
+    Browser* browser = displayer.browser();
+    content::WebContents* page = browser->OpenURL(content::OpenURLParams(
+        GURL(chrome::kChromeUIUntrustedCroshURL), content::Referrer(),
+        WindowOpenDisposition::NEW_FOREGROUND_TAB,
+        ui::PAGE_TRANSITION_GENERATED, false));
+    browser->window()->Show();
+    browser->window()->Activate();
+    page->Focus();
+  }
 }
 
 void ChromeNewWindowClient::OpenGetHelp() {
diff --git a/chrome/browser/web_applications/components/os_integration_manager_unittest.cc b/chrome/browser/web_applications/components/os_integration_manager_unittest.cc
index d63a74d..a70d228d 100644
--- a/chrome/browser/web_applications/components/os_integration_manager_unittest.cc
+++ b/chrome/browser/web_applications/components/os_integration_manager_unittest.cc
@@ -17,6 +17,7 @@
 #include "chrome/browser/web_applications/components/web_app_constants.h"
 #include "chrome/browser/web_applications/web_app.h"
 #include "chrome/common/chrome_constants.h"
+#include "chrome/common/chrome_features.h"
 #include "content/public/test/browser_task_environment.h"
 #include "testing/gmock/include/gmock/gmock.h"
 #include "testing/gtest/include/gtest/gtest.h"
@@ -154,19 +155,23 @@
 #endif  // defined(OS_WIN)
 
 const char kFakeAppUrl[] = "https://fake.com";
+const std::u16string kFakeAppTitle(u"fake title");
 
 std::unique_ptr<ShortcutInfo> CreateTestShorcutInfo(const AppId& app_id) {
   auto shortcut_info = std::make_unique<ShortcutInfo>();
   shortcut_info->profile_path = base::FilePath(kFakeProfilePath);
   shortcut_info->extension_id = app_id;
   shortcut_info->url = GURL(kFakeAppUrl);
+  shortcut_info->title = kFakeAppTitle;
   return shortcut_info;
 }
 
 class OsIntegrationManagerTest : public testing::Test {
  public:
   OsIntegrationManagerTest() {
-    features_.InitAndEnableFeature(blink::features::kWebAppEnableUrlHandlers);
+    features_.InitWithFeatures({blink::features::kWebAppEnableUrlHandlers,
+                                ::features::kDesktopPWAsRunOnOsLogin},
+                               {});
   }
 
   ~OsIntegrationManagerTest() override = default;
@@ -226,6 +231,7 @@
   EXPECT_CALL(manager, ReadAllShortcutsMenuIconsAndRegisterShortcutsMenu(
                            app_id, testing::_))
       .WillOnce(base::test::RunOnceCallback<1>(true));
+  EXPECT_CALL(manager, RegisterRunOnOsLogin(app_id, testing::_)).Times(1);
 
   InstallOsHooksOptions options;
   options.add_to_desktop = true;
@@ -277,6 +283,10 @@
   EXPECT_CALL(manager, UnregisterWebAppOsUninstallation(app_id)).Times(1);
   EXPECT_CALL(manager, UnregisterShortcutsMenu(app_id))
       .WillOnce(testing::Return(true));
+  EXPECT_CALL(manager,
+              UnregisterRunOnOsLogin(app_id, base::FilePath(kFakeProfilePath),
+                                     kFakeAppTitle, testing::_))
+      .Times(1);
 
   manager.UninstallAllOsHooks(app_id, std::move(callback));
   run_loop.Run();
diff --git a/chrome/build/linux.pgo.txt b/chrome/build/linux.pgo.txt
index a8e16ce..c7bfb2cd 100644
--- a/chrome/build/linux.pgo.txt
+++ b/chrome/build/linux.pgo.txt
@@ -1 +1 @@
-chrome-linux-master-1616759954-345f4e8846912b97ff232ef62df676ed769a64cd.profdata
+chrome-linux-master-1616909642-b8d668688aa21d581631333d1d259b3ee53161e5.profdata
diff --git a/chrome/build/mac.pgo.txt b/chrome/build/mac.pgo.txt
index c7b3e004..fa9f689 100644
--- a/chrome/build/mac.pgo.txt
+++ b/chrome/build/mac.pgo.txt
@@ -1 +1 @@
-chrome-mac-master-1616759954-99866b761dcad11414ec9ebb20e1d43cd0dac5c5.profdata
+chrome-mac-master-1616909642-cadebb3e3874de51e4b2e526d8a64c952ce04701.profdata
diff --git a/chrome/build/win32.pgo.txt b/chrome/build/win32.pgo.txt
index c061c4cc..f9043e4 100644
--- a/chrome/build/win32.pgo.txt
+++ b/chrome/build/win32.pgo.txt
@@ -1 +1 @@
-chrome-win32-master-1616748703-f8bdc10e4d2e08b1638f9b036f6c1bdce7a348ea.profdata
+chrome-win32-master-1616909642-b3eb7daf8a769d0b70e30ffb0035c5d97d57d140.profdata
diff --git a/chrome/build/win64.pgo.txt b/chrome/build/win64.pgo.txt
index 6475eb5..6ad78ec 100644
--- a/chrome/build/win64.pgo.txt
+++ b/chrome/build/win64.pgo.txt
@@ -1 +1 @@
-chrome-win64-master-1616759954-bc21f04558fbce24839a5ad4a05c32f18f01afd9.profdata
+chrome-win64-master-1616909642-1e3268681d5ce4b0a1d37afc109b045126e42f09.profdata
diff --git a/chrome/common/channel_info.h b/chrome/common/channel_info.h
index 8881e31..dd47a46e 100644
--- a/chrome/common/channel_info.h
+++ b/chrome/common/channel_info.h
@@ -30,8 +30,7 @@
 // installs appear identical to regular stable unless `with_extended_stable` is
 // true.
 // Prefer version_info::GetVersionNumber() if only the version number is needed.
-std::string GetVersionString(
-    WithExtendedStable with_extended_stable = WithExtendedStable(false));
+std::string GetVersionString(WithExtendedStable with_extended_stable);
 
 // Returns the name of the browser's update channel. For a branded build, this
 // modifier is the channel ("canary", "dev", or "beta", but "" for stable).
@@ -49,8 +48,7 @@
 // cannot be determined, "unknown" will be returned. In unbranded builds, the
 // modifier is usually an empty string (""), although on Linux, it may vary in
 // certain distributions. To simply test the channel, use GetChannel().
-std::string GetChannelName(
-    WithExtendedStable with_extended_stable = WithExtendedStable(false));
+std::string GetChannelName(WithExtendedStable with_extended_stable);
 
 // Returns the channel for the installation. In branded builds, this will be
 // version_info::Channel::{STABLE,BETA,DEV,CANARY}. In unbranded builds, or
diff --git a/chrome/common/pref_names.cc b/chrome/common/pref_names.cc
index 906dd41..da8356c4 100644
--- a/chrome/common/pref_names.cc
+++ b/chrome/common/pref_names.cc
@@ -2171,6 +2171,9 @@
 // from http to https.
 const char kHSTSPolicyBypassList[] = "hsts.policy.upgrade_bypass_list";
 
+// If false, disable post-quantum key agreement in TLS connections.
+const char kCECPQ2Enabled[] = "ssl.cecpq2_enabled";
+
 // Boolean that specifies whether the built-in asynchronous DNS client is used.
 const char kBuiltInDnsClientEnabled[] = "async_dns.enabled";
 
diff --git a/chrome/common/pref_names.h b/chrome/common/pref_names.h
index db49fff..bc8bada 100644
--- a/chrome/common/pref_names.h
+++ b/chrome/common/pref_names.h
@@ -829,6 +829,7 @@
 extern const char kCipherSuiteBlacklist[];
 extern const char kH2ClientCertCoalescingHosts[];
 extern const char kHSTSPolicyBypassList[];
+extern const char kCECPQ2Enabled[];
 
 extern const char kBuiltInDnsClientEnabled[];
 extern const char kDnsOverHttpsMode[];
diff --git a/chrome/test/BUILD.gn b/chrome/test/BUILD.gn
index 1eb47c6..5fa36f22 100644
--- a/chrome/test/BUILD.gn
+++ b/chrome/test/BUILD.gn
@@ -553,6 +553,7 @@
       "//build:chromeos_buildflags",
       "//chrome:chrome_android_core",
       "//chrome/android:app_hooks_java",
+      "//chrome/browser/flags:flags_android",
       "//chrome/browser/metrics:test_support",
       "//chrome/browser/payments:browser_tests",
       "//chrome/browser/privacy_budget:browser_tests",
diff --git a/chrome/test/data/policy/policy_test_cases.json b/chrome/test/data/policy/policy_test_cases.json
index 9b4aac2..58f317d 100644
--- a/chrome/test/data/policy/policy_test_cases.json
+++ b/chrome/test/data/policy/policy_test_cases.json
@@ -3203,6 +3203,16 @@
     "note": "This policy has been removed, see https://crbug.com/939760."
   },
 
+  "CECPQ2Enabled": {
+    "os": ["win", "linux", "mac", "chromeos", "android"],
+    "policy_pref_mapping_tests": [
+      {
+        "policies": { "CECPQ2Enabled": true },
+        "prefs": { "ssl.cecpq2_enabled": { "location": "local_state" } }
+      }
+    ]
+  },
+
   "CertificateTransparencyEnforcementDisabledForUrls": {
     "os": ["win", "linux", "mac", "chromeos", "android"],
     "policy_pref_mapping_tests": [
diff --git a/chromeos/profiles/atom.afdo.newest.txt b/chromeos/profiles/atom.afdo.newest.txt
index 7ada68e..5cefedd 100644
--- a/chromeos/profiles/atom.afdo.newest.txt
+++ b/chromeos/profiles/atom.afdo.newest.txt
@@ -1 +1 @@
-chromeos-chrome-amd64-atom-91-4449.0-1616407110-benchmark-91.0.4456.0-r1-redacted.afdo.xz
+chromeos-chrome-amd64-atom-91-4449.0-1616407110-benchmark-91.0.4459.0-r1-redacted.afdo.xz
diff --git a/chromeos/profiles/bigcore.afdo.newest.txt b/chromeos/profiles/bigcore.afdo.newest.txt
index 7e1a438a..4b5f1cc 100644
--- a/chromeos/profiles/bigcore.afdo.newest.txt
+++ b/chromeos/profiles/bigcore.afdo.newest.txt
@@ -1 +1 @@
-chromeos-chrome-amd64-bigcore-91-4430.30-1616405964-benchmark-91.0.4456.0-r1-redacted.afdo.xz
+chromeos-chrome-amd64-bigcore-91-4430.30-1616405964-benchmark-91.0.4459.0-r1-redacted.afdo.xz
diff --git a/chromeos/profiles/orderfile.newest.txt b/chromeos/profiles/orderfile.newest.txt
index 493556bc..c4d17217 100644
--- a/chromeos/profiles/orderfile.newest.txt
+++ b/chromeos/profiles/orderfile.newest.txt
@@ -1 +1 @@
-chromeos-chrome-orderfile-field-91-4449.0-1616407110-benchmark-91.0.4455.0-r1.orderfile.xz
+chromeos-chrome-orderfile-field-91-4449.0-1616407110-benchmark-91.0.4456.0-r1.orderfile.xz
diff --git a/components/policy/resources/policy_templates.json b/components/policy/resources/policy_templates.json
index 2b467ee..09a4e975 100644
--- a/components/policy/resources/policy_templates.json
+++ b/components/policy/resources/policy_templates.json
@@ -14426,6 +14426,41 @@
       Otherwise it may be set to one of the following values: "tls1.2" or "tls1.3". When set, <ph name="PRODUCT_NAME">$1<ex>Google Chrome</ex></ph> will not use SSL/TLS versions greater than the specified version. An unrecognized value will be ignored.''',
     },
     {
+      'name': 'CECPQ2Enabled',
+      'owners': ['file://crypto/OWNERS'],
+      'type': 'main',
+      'example_value': True,
+      'default': True,
+      'schema': { 'type': 'boolean' },
+      'items': [
+        {
+          'value': True,
+          'caption': 'Enable default CECPQ2 rollout process',
+        },
+        {
+          'value': False,
+          'caption': 'Disable CECPQ2',
+        },
+      ],
+      'supported_on': [
+        'chrome.*:91-',
+        'chrome_os:91-',
+        'android:91-',
+      ],
+      'features': {
+        'dynamic_refresh': True,
+        'per_profile': False,
+      },
+      'id': 841,
+      'caption': '''CECPQ2 post-quantum key-agreement enabled for TLS''',
+      'tags': ['system-security'],
+      'desc': '''If this policy is not configured, or is set to enabled, then <ph name="PRODUCT_NAME">$1<ex>Google Chrome</ex></ph> will follow the default rollout process for CECPQ2, a post-quantum key-agreement algorithm in TLS.
+
+      CECPQ2 results in larger TLS messages which, in very rare cases, can trigger bugs in some networking hardware. This policy can be set to False to disable CECPQ2 while networking issues are resolved.
+
+      This policy is a temporary measure and will be removed in future versions of <ph name="PRODUCT_NAME">$1<ex>Google Chrome</ex></ph>.''',
+    },
+    {
       'name': 'CertificateTransparencyEnforcementDisabledForUrls',
       'owners': ['file://components/certificate_transparency/OWNERS', 'rsleevi@chromium.org'],
       'type': 'list',
@@ -26120,6 +26155,6 @@
   'placeholders': [],
   'deleted_policy_ids': [114, 115, 204, 205, 206, 412, 476, 544, 546, 562, 569, 578, 583, 585, 586, 587, 588, 589, 590, 591, 600, 668, 669],
   'deleted_atomic_policy_group_ids': [19],
-  'highest_id_currently_used': 840,
+  'highest_id_currently_used': 841,
   'highest_atomic_group_id_currently_used': 40
 }
diff --git a/components/viz/common/features.cc b/components/viz/common/features.cc
index eee47ca..951add3 100644
--- a/components/viz/common/features.cc
+++ b/components/viz/common/features.cc
@@ -120,6 +120,11 @@
                                    base::FEATURE_DISABLED_BY_DEFAULT};
 #endif
 
+// Enables platform supported delegated ink trails instead of Skia backed
+// delegated ink trails.
+const base::Feature kUsePlatformDelegatedInk{"UsePlatformDelegatedInk",
+                                             base::FEATURE_DISABLED_BY_DEFAULT};
+
 // Used to debug Android WebView Vulkan composite. Composite to an intermediate
 // buffer and draw the intermediate buffer to the secondary command buffer.
 const base::Feature kWebViewVulkanIntermediateBuffer{
@@ -251,4 +256,8 @@
   return base::nullopt;
 }
 
+bool ShouldUsePlatformDelegatedInk() {
+  return base::FeatureList::IsEnabled(kUsePlatformDelegatedInk);
+}
+
 }  // namespace features
diff --git a/components/viz/common/features.h b/components/viz/common/features.h
index 5161da6..c165f44 100644
--- a/components/viz/common/features.h
+++ b/components/viz/common/features.h
@@ -38,6 +38,7 @@
 VIZ_COMMON_EXPORT extern const base::Feature kUseX11Present;
 #endif
 VIZ_COMMON_EXPORT extern const base::Feature kWebViewVulkanIntermediateBuffer;
+VIZ_COMMON_EXPORT extern const base::Feature kUsePlatformDelegatedInk;
 
 VIZ_COMMON_EXPORT bool IsAdpfEnabled();
 #if defined(OS_ANDROID)
@@ -57,6 +58,7 @@
 VIZ_COMMON_EXPORT bool ShouldUseSetPresentDuration();
 #endif  // OS_WIN
 VIZ_COMMON_EXPORT base::Optional<int> ShouldDrawPredictedInkPoints();
+VIZ_COMMON_EXPORT bool ShouldUsePlatformDelegatedInk();
 
 }  // namespace features
 
diff --git a/components/viz/service/BUILD.gn b/components/viz/service/BUILD.gn
index 2ffc8a6d..159fa9e 100644
--- a/components/viz/service/BUILD.gn
+++ b/components/viz/service/BUILD.gn
@@ -24,6 +24,8 @@
     "display/bsp_walk_action.h",
     "display/damage_frame_annotator.cc",
     "display/damage_frame_annotator.h",
+    "display/delegated_ink_handler.cc",
+    "display/delegated_ink_handler.h",
     "display/delegated_ink_point_renderer_base.cc",
     "display/delegated_ink_point_renderer_base.h",
     "display/delegated_ink_point_renderer_skia.cc",
diff --git a/components/viz/service/display/delegated_ink_handler.cc b/components/viz/service/display/delegated_ink_handler.cc
new file mode 100644
index 0000000..6f15703
--- /dev/null
+++ b/components/viz/service/display/delegated_ink_handler.cc
@@ -0,0 +1,51 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "components/viz/service/display/delegated_ink_handler.h"
+
+#include <utility>
+
+#include "components/viz/common/features.h"
+#include "components/viz/service/display/delegated_ink_point_renderer_skia.h"
+#include "ui/gfx/delegated_ink_metadata.h"
+
+namespace viz {
+DelegatedInkHandler::DelegatedInkHandler(bool platform_supports_delegated_ink)
+    : use_delegated_ink_renderer_(!platform_supports_delegated_ink ||
+                                  !features::ShouldUsePlatformDelegatedInk()) {
+  if (use_delegated_ink_renderer_)
+    ink_data_ = std::make_unique<DelegatedInkPointRendererSkia>();
+}
+DelegatedInkHandler::~DelegatedInkHandler() = default;
+
+void DelegatedInkHandler::SetDelegatedInkMetadata(MetadataUniquePtr metadata) {
+  if (use_delegated_ink_renderer_) {
+    absl::get<RendererUniquePtr>(ink_data_)->SetDelegatedInkMetadata(
+        std::move(metadata));
+  } else {
+    ink_data_ = std::move(metadata);
+  }
+}
+
+DelegatedInkHandler::MetadataUniquePtr DelegatedInkHandler::TakeMetadata() {
+  if (!use_delegated_ink_renderer_)
+    return std::move(absl::get<MetadataUniquePtr>(ink_data_));
+
+  return nullptr;
+}
+
+DelegatedInkPointRendererSkia* DelegatedInkHandler::GetInkRenderer() {
+  if (use_delegated_ink_renderer_)
+    return absl::get<RendererUniquePtr>(ink_data_).get();
+
+  return nullptr;
+}
+
+void DelegatedInkHandler::SetDelegatedInkPointRendererForTest(
+    RendererUniquePtr renderer) {
+  DCHECK(use_delegated_ink_renderer_);
+  ink_data_ = std::move(renderer);
+}
+
+}  // namespace viz
diff --git a/components/viz/service/display/delegated_ink_handler.h b/components/viz/service/display/delegated_ink_handler.h
new file mode 100644
index 0000000..a4f9283
--- /dev/null
+++ b/components/viz/service/display/delegated_ink_handler.h
@@ -0,0 +1,44 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef COMPONENTS_VIZ_SERVICE_DISPLAY_DELEGATED_INK_HANDLER_H_
+#define COMPONENTS_VIZ_SERVICE_DISPLAY_DELEGATED_INK_HANDLER_H_
+
+#include <memory>
+
+#include "third_party/abseil-cpp/absl/types/variant.h"
+
+namespace gfx {
+class DelegatedInkMetadata;
+}  // namespace gfx
+
+namespace viz {
+class DelegatedInkPointRendererSkia;
+
+class DelegatedInkHandler {
+ public:
+  using MetadataUniquePtr = std::unique_ptr<gfx::DelegatedInkMetadata>;
+  using RendererUniquePtr = std::unique_ptr<DelegatedInkPointRendererSkia>;
+
+  explicit DelegatedInkHandler(bool platform_supports_delegated_ink);
+  ~DelegatedInkHandler();
+
+  void SetDelegatedInkMetadata(MetadataUniquePtr metadata);
+
+  MetadataUniquePtr TakeMetadata();
+  DelegatedInkPointRendererSkia* GetInkRenderer();
+
+ protected:
+  friend class SkiaRenderer;
+  // Used for testing only, to set up test ink renderers.
+  void SetDelegatedInkPointRendererForTest(RendererUniquePtr renderer);
+
+ private:
+  const bool use_delegated_ink_renderer_;
+  absl::variant<MetadataUniquePtr, RendererUniquePtr> ink_data_;
+};
+
+}  // namespace viz
+
+#endif  // COMPONENTS_VIZ_SERVICE_DISPLAY_DELEGATED_INK_HANDLER_H_
diff --git a/components/viz/service/display/delegated_ink_point_pixel_test_helper.cc b/components/viz/service/display/delegated_ink_point_pixel_test_helper.cc
index 3f21399..186451d 100644
--- a/components/viz/service/display/delegated_ink_point_pixel_test_helper.cc
+++ b/components/viz/service/display/delegated_ink_point_pixel_test_helper.cc
@@ -2,10 +2,11 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-#include <memory>
-
 #include "components/viz/service/display/delegated_ink_point_pixel_test_helper.h"
 
+#include <memory>
+#include <utility>
+
 #include "components/viz/service/display/direct_renderer.h"
 #include "testing/gtest/include/gtest/gtest.h"
 #include "ui/gfx/geometry/rect_conversions.h"
@@ -18,18 +19,19 @@
 DelegatedInkPointPixelTestHelper::DelegatedInkPointPixelTestHelper(
     DirectRenderer* renderer)
     : renderer_(renderer) {
-  renderer_->CreateDelegatedInkPointRenderer();
+  CreateInkRenderer();
 }
 
 void DelegatedInkPointPixelTestHelper::SetRendererAndCreateInkRenderer(
     DirectRenderer* renderer) {
   renderer_ = renderer;
-  renderer_->CreateDelegatedInkPointRenderer();
+  CreateInkRenderer();
 }
 
-DelegatedInkPointRendererBase*
-DelegatedInkPointPixelTestHelper::GetInkRenderer() {
-  return renderer_->GetDelegatedInkPointRenderer();
+void DelegatedInkPointPixelTestHelper::CreateInkRenderer() {
+  auto ink_renderer = std::make_unique<DelegatedInkPointRendererSkia>();
+  ink_renderer_ = ink_renderer.get();
+  renderer_->SetDelegatedInkPointRendererSkiaForTest(std::move(ink_renderer));
 }
 
 void DelegatedInkPointPixelTestHelper::CreateAndSendMetadata(
@@ -42,7 +44,7 @@
   metadata_ = gfx::DelegatedInkMetadata(
       point, diameter, color, timestamp, presentation_area,
       base::TimeTicks::Now(), /*hovering*/ false);
-  GetInkRenderer()->SetDelegatedInkMetadata(
+  ink_renderer_->SetDelegatedInkMetadata(
       std::make_unique<gfx::DelegatedInkMetadata>(metadata_));
 }
 
@@ -72,7 +74,7 @@
     int32_t pointer_id) {
   DCHECK(renderer_);
   ink_points_[pointer_id].emplace_back(point, timestamp, pointer_id);
-  GetInkRenderer()->StoreDelegatedInkPoint(ink_points_[pointer_id].back());
+  ink_renderer_->StoreDelegatedInkPoint(ink_points_[pointer_id].back());
 }
 
 void DelegatedInkPointPixelTestHelper::CreateAndSendPointFromLastPoint(
diff --git a/components/viz/service/display/delegated_ink_point_pixel_test_helper.h b/components/viz/service/display/delegated_ink_point_pixel_test_helper.h
index d13fa68..7b8f162 100644
--- a/components/viz/service/display/delegated_ink_point_pixel_test_helper.h
+++ b/components/viz/service/display/delegated_ink_point_pixel_test_helper.h
@@ -34,7 +34,6 @@
 
   explicit DelegatedInkPointPixelTestHelper(DirectRenderer* renderer);
   void SetRendererAndCreateInkRenderer(DirectRenderer* renderer);
-  DelegatedInkPointRendererBase* GetInkRenderer();
 
   void CreateAndSendMetadata(const gfx::PointF& point,
                              float diameter,
@@ -62,7 +61,10 @@
   const gfx::DelegatedInkMetadata& metadata() { return metadata_; }
 
  private:
+  void CreateInkRenderer();
+
   DirectRenderer* renderer_ = nullptr;
+  DelegatedInkPointRendererBase* ink_renderer_ = nullptr;
   std::unordered_map<int32_t, std::vector<gfx::DelegatedInkPoint>> ink_points_;
   gfx::DelegatedInkMetadata metadata_;
 };
diff --git a/components/viz/service/display/delegated_ink_point_renderer_base.h b/components/viz/service/display/delegated_ink_point_renderer_base.h
index aa28cdd..00334339 100644
--- a/components/viz/service/display/delegated_ink_point_renderer_base.h
+++ b/components/viz/service/display/delegated_ink_point_renderer_base.h
@@ -43,7 +43,7 @@
       mojo::PendingReceiver<mojom::DelegatedInkPointRenderer> receiver);
 
   void StoreDelegatedInkPoint(const gfx::DelegatedInkPoint& point) override;
-  void SetDelegatedInkMetadata(
+  virtual void SetDelegatedInkMetadata(
       std::unique_ptr<gfx::DelegatedInkMetadata> metadata);
 
   virtual void FinalizePathForDraw() = 0;
@@ -63,6 +63,7 @@
   std::unique_ptr<gfx::DelegatedInkMetadata> metadata_;
 
  private:
+  friend class DelegatedInkDisplayTest;
   friend class SkiaDelegatedInkRendererTest;
 
   const std::unordered_map<int32_t, DelegatedInkTrailData>&
diff --git a/components/viz/service/display/direct_renderer.cc b/components/viz/service/display/direct_renderer.cc
index 0dafacec..46bab49 100644
--- a/components/viz/service/display/direct_renderer.cc
+++ b/components/viz/service/display/direct_renderer.cc
@@ -252,8 +252,8 @@
     current_frame()->root_damage_rect.Union(
         overlay_processor_->GetAndResetOverlayDamage());
   }
-  if (DelegatedInkPointRendererBase* ink_renderer =
-          GetDelegatedInkPointRenderer()) {
+  if (auto* ink_renderer =
+          GetDelegatedInkPointRenderer(/*create_if_necessary=*/false)) {
     // The path must be finalized before GetDamageRect() can return an accurate
     // rect that will allow the old trail to be removed and the new trail to
     // be drawn at the same time.
@@ -986,22 +986,11 @@
       current_frame()->current_render_pass->content_color_usage);
 }
 
-bool DirectRenderer::CreateDelegatedInkPointRenderer() {
-  return false;
-}
-
-DelegatedInkPointRendererBase* DirectRenderer::GetDelegatedInkPointRenderer() {
+DelegatedInkPointRendererBase* DirectRenderer::GetDelegatedInkPointRenderer(
+    bool create_if_necessary) {
   return nullptr;
 }
 
-void DirectRenderer::SetDelegatedInkMetadata(
-    std::unique_ptr<gfx::DelegatedInkMetadata> metadata) {
-  if (!GetDelegatedInkPointRenderer() && !CreateDelegatedInkPointRenderer())
-    return;
-
-  GetDelegatedInkPointRenderer()->SetDelegatedInkMetadata(std::move(metadata));
-}
-
 void DirectRenderer::DrawDelegatedInkTrail() {
   NOTREACHED();
 }
@@ -1013,10 +1002,12 @@
 void DirectRenderer::AddCompositeTimeTraces(base::TimeTicks ready_timestamp) {}
 
 gfx::Rect DirectRenderer::GetDelegatedInkTrailDamageRect() {
-  if (!GetDelegatedInkPointRenderer())
-    return gfx::Rect();
+  if (auto* ink_renderer =
+          GetDelegatedInkPointRenderer(/*create_if_necessary=*/false)) {
+    return ink_renderer->GetDamageRect();
+  }
 
-  return GetDelegatedInkPointRenderer()->GetDamageRect();
+  return gfx::Rect();
 }
 
 }  // namespace viz
diff --git a/components/viz/service/display/direct_renderer.h b/components/viz/service/display/direct_renderer.h
index 79b344bb..9268fca 100644
--- a/components/viz/service/display/direct_renderer.h
+++ b/components/viz/service/display/direct_renderer.h
@@ -18,7 +18,7 @@
 #include "components/viz/common/quads/aggregated_render_pass.h"
 #include "components/viz/common/quads/tile_draw_quad.h"
 #include "components/viz/service/display/aggregated_frame.h"
-#include "components/viz/service/display/delegated_ink_point_renderer_base.h"
+#include "components/viz/service/display/delegated_ink_point_renderer_skia.h"
 #include "components/viz/service/display/display_resource_provider.h"
 #include "components/viz/service/display/overlay_candidate.h"
 #include "components/viz/service/display/overlay_processor_interface.h"
@@ -141,9 +141,10 @@
     return last_root_render_pass_scissor_rect_;
   }
 
-  virtual DelegatedInkPointRendererBase* GetDelegatedInkPointRenderer();
-  void SetDelegatedInkMetadata(
-      std::unique_ptr<gfx::DelegatedInkMetadata> metadata);
+  virtual DelegatedInkPointRendererBase* GetDelegatedInkPointRenderer(
+      bool create_if_necessary);
+  virtual void SetDelegatedInkMetadata(
+      std::unique_ptr<gfx::DelegatedInkMetadata> metadata) {}
 
   // Returns true if composite time tracing is enabled. This measures a detailed
   // trace log for draw time spent per quad.
@@ -159,6 +160,7 @@
   friend class BspWalkActionDrawPolygon;
   friend class SkiaDelegatedInkRendererTest;
   friend class DelegatedInkPointPixelTestHelper;
+  friend class DelegatedInkDisplayTest;
 
   enum SurfaceInitializationMode {
     SURFACE_INITIALIZATION_MODE_PRESERVE,
@@ -336,10 +338,10 @@
   }
   gfx::ColorSpace reshape_color_space() const { return reshape_color_space_; }
 
-  // Return a bool to inform the caller if the delegated ink renderer was
-  // actually created or not. If the renderer doesn't support drawing delegated
-  // ink trails, then the delegated ink renderer won't be created.
-  virtual bool CreateDelegatedInkPointRenderer();
+  // Sets a DelegatedInkPointRendererSkiaForTest to be used for testing only, in
+  // order to save delegated ink metadata values that would otherwise be reset.
+  virtual void SetDelegatedInkPointRendererSkiaForTest(
+      std::unique_ptr<DelegatedInkPointRendererSkia> renderer) {}
 
  private:
   virtual void DrawDelegatedInkTrail();
diff --git a/components/viz/service/display/display.cc b/components/viz/service/display/display.cc
index 32d1b48d..e882f76 100644
--- a/components/viz/service/display/display.cc
+++ b/components/viz/service/display/display.cc
@@ -1334,8 +1334,9 @@
   }
 }
 
-DelegatedInkPointRendererBase* Display::GetDelegatedInkPointRenderer() {
-  return renderer_->GetDelegatedInkPointRenderer();
+DelegatedInkPointRendererBase* Display::GetDelegatedInkPointRenderer(
+    bool create_if_necessary) {
+  return renderer_->GetDelegatedInkPointRenderer(create_if_necessary);
 }
 
 }  // namespace viz
diff --git a/components/viz/service/display/display.h b/components/viz/service/display/display.h
index 8d2a7f2..09d46308 100644
--- a/components/viz/service/display/display.h
+++ b/components/viz/service/display/display.h
@@ -198,7 +198,8 @@
   // Return the delegated ink point renderer from |renderer_|, creating it if
   // one doesn't exist. Should only be used when the delegated ink trails web
   // API has been used.
-  DelegatedInkPointRendererBase* GetDelegatedInkPointRenderer();
+  DelegatedInkPointRendererBase* GetDelegatedInkPointRenderer(
+      bool create_if_necessary);
 
  private:
   friend class DisplayTest;
diff --git a/components/viz/service/display/display_unittest.cc b/components/viz/service/display/display_unittest.cc
index d332963e..9e8c02c 100644
--- a/components/viz/service/display/display_unittest.cc
+++ b/components/viz/service/display/display_unittest.cc
@@ -6,6 +6,7 @@
 
 #include <limits>
 #include <map>
+#include <string>
 #include <unordered_map>
 #include <utility>
 
@@ -17,8 +18,10 @@
 #include "base/test/bind.h"
 #include "base/test/metrics/histogram_tester.h"
 #include "base/test/null_task_runner.h"
+#include "base/test/scoped_feature_list.h"
 #include "cc/base/math_util.h"
 #include "cc/test/scheduler_test_common.h"
+#include "components/viz/common/features.h"
 #include "components/viz/common/frame_sinks/begin_frame_source.h"
 #include "components/viz/common/frame_sinks/copy_output_request.h"
 #include "components/viz/common/frame_sinks/copy_output_result.h"
@@ -46,6 +49,7 @@
 #include "components/viz/service/surfaces/surface.h"
 #include "components/viz/service/surfaces/surface_manager.h"
 #include "components/viz/test/compositor_frame_helpers.h"
+#include "components/viz/test/delegated_ink_point_renderer_skia_for_test.h"
 #include "components/viz/test/fake_output_surface.h"
 #include "components/viz/test/fake_skia_output_surface.h"
 #include "components/viz/test/mock_compositor_frame_sink_client.h"
@@ -4535,13 +4539,18 @@
     SetUpGpuDisplaySkia(settings);
 
     // Initialize the renderer and create an ink renderer.
-    StubDisplayClient client;
-    display_->Initialize(&client, manager_.surface_manager());
-    display_->renderer_for_testing()->CreateDelegatedInkPointRenderer();
+    display_->Initialize(&client_, manager_.surface_manager());
+
+    auto renderer = std::make_unique<DelegatedInkPointRendererSkiaForTest>();
+    ink_renderer_ = renderer.get();
+    display_->renderer_for_testing()->SetDelegatedInkPointRendererSkiaForTest(
+        std::move(renderer));
   }
 
   DelegatedInkPointRendererBase* ink_renderer() {
-    return display_->renderer_for_testing()->GetDelegatedInkPointRenderer();
+    return display_->renderer_for_testing()
+        ->GetDelegatedInkPointRenderer(/*create_if_necessary=*/
+                                       false);
   }
 
   int UniqueStoredPointerIds() {
@@ -4695,6 +4704,12 @@
     return ink_points_[pointer_id].size();
   }
 
+ protected:
+  DelegatedInkPointRendererSkiaForTest* ink_renderer_ = nullptr;
+
+  // Stub client kept in scope to prevent access violations during DrawAndSwap.
+  StubDisplayClient client_;
+
  private:
   std::unordered_map<int32_t, std::vector<gfx::DelegatedInkPoint>> ink_points_;
 };
@@ -4972,4 +4987,124 @@
                                  base::TimeDelta::Min());
 }
 
+enum class DelegatedInkType { kPlatformInk, kSkiaInk };
+
+class DelegatedInkDisplayTest
+    : public SkiaDelegatedInkRendererTest,
+      public testing::WithParamInterface<DelegatedInkType> {
+ public:
+  void SetUpGpuDisplaySkiaWithPlatformInk(const RendererSettings& settings) {
+    scoped_refptr<TestContextProvider> provider = TestContextProvider::Create();
+    provider->BindToCurrentThread();
+    std::unique_ptr<FakeSkiaOutputSurface> skia_output_surface =
+        FakeSkiaOutputSurface::Create3d(std::move(provider));
+    // Set the delegated ink capability on the output surface to true so that
+    // path can be tested in Display::DrawAndSwap
+    skia_output_surface->UsePlatformDelegatedInkForTesting();
+    skia_output_surface_ = skia_output_surface.get();
+
+    CreateDisplaySchedulerAndDisplay(settings, kArbitraryFrameSinkId,
+                                     std::move(skia_output_surface));
+  }
+
+  void SetUpGpuDisplay() {
+    if (GetParam() == DelegatedInkType::kSkiaInk) {
+      SetUpRenderers();
+    } else {
+      scoped_feature_list_.InitAndEnableFeature(
+          features::kUsePlatformDelegatedInk);
+
+      // Set up the display to use the Skia renderer.
+      RendererSettings settings;
+      settings.use_skia_renderer = true;
+      SetUpGpuDisplaySkiaWithPlatformInk(settings);
+
+      display_->Initialize(&client_, manager_.surface_manager());
+    }
+  }
+
+  void SubmitCompositorFrameWithInkMetadata(
+      CompositorRenderPassList* pass_list,
+      const LocalSurfaceId& local_surface_id,
+      const gfx::DelegatedInkMetadata& metadata) {
+    CompositorFrame frame = CompositorFrameBuilder()
+                                .SetRenderPassList(std::move(*pass_list))
+                                .AddDelegatedInkMetadata(metadata)
+                                .Build();
+    pass_list->clear();
+
+    support_->SubmitCompositorFrame(local_surface_id, std::move(frame));
+  }
+
+  const gfx::DelegatedInkMetadata* GetMetadataFromTestRenderer() {
+    return ink_renderer_->last_metadata();
+  }
+
+ private:
+  base::test::ScopedFeatureList scoped_feature_list_;
+};
+
+struct DelegatedInkDisplayTestPassToString {
+  std::string operator()(
+      const testing::TestParamInfo<DelegatedInkType> type) const {
+    return type.param == DelegatedInkType::kPlatformInk ? "PlatformInk"
+                                                        : "SkiaInk";
+  }
+};
+
+INSTANTIATE_TEST_SUITE_P(DelegatedInkTrails,
+                         DelegatedInkDisplayTest,
+                         testing::Values(DelegatedInkType::kPlatformInk,
+                                         DelegatedInkType::kSkiaInk),
+                         DelegatedInkDisplayTestPassToString());
+
+// Confirm that delegated ink metadata is not ever sent to both the delegated
+// ink renderer and the output surface (for platform delegated ink), only one
+// or the other.
+TEST_P(DelegatedInkDisplayTest, MetadataOnlySentToSkiaRendererOrOutputSurface) {
+  SetUpGpuDisplay();
+
+  id_allocator_.GenerateId();
+  display_->SetLocalSurfaceId(id_allocator_.GetCurrentLocalSurfaceId(), 1.f);
+  display_->Resize(gfx::Size(100, 100));
+
+  CompositorRenderPassList pass_list;
+  auto pass = CompositorRenderPass::Create();
+  pass->output_rect = gfx::Rect(0, 0, 100, 100);
+  pass->damage_rect = gfx::Rect(10, 10, 1, 1);
+  pass->id = CompositorRenderPassId{1u};
+  pass_list.push_back(std::move(pass));
+
+  gfx::DelegatedInkMetadata metadata(
+      gfx::PointF(5, 5), 3.5f, SK_ColorBLACK, base::TimeTicks::Now(),
+      gfx::RectF(0, 0, 20, 20), base::TimeTicks::Now(), false);
+
+  SubmitCompositorFrameWithInkMetadata(
+      &pass_list, id_allocator_.GetCurrentLocalSurfaceId(), metadata);
+  display_->DrawAndSwap(base::TimeTicks::Now());
+
+  // Confirm that the metadata correctly made it to either the skia output
+  // surface, or the delegated ink renderer.
+  const gfx::DelegatedInkMetadata* retrieved_metadata =
+      GetParam() == DelegatedInkType::kPlatformInk
+          ? skia_output_surface_->last_delegated_ink_metadata()
+          : GetMetadataFromTestRenderer();
+  EXPECT_TRUE(retrieved_metadata);
+  EXPECT_EQ(retrieved_metadata->point(), metadata.point());
+  EXPECT_EQ(retrieved_metadata->diameter(), metadata.diameter());
+  EXPECT_EQ(retrieved_metadata->color(), metadata.color());
+  EXPECT_EQ(retrieved_metadata->timestamp(), metadata.timestamp());
+  EXPECT_EQ(retrieved_metadata->presentation_area(),
+            metadata.presentation_area());
+  EXPECT_EQ(retrieved_metadata->is_hovering(), metadata.is_hovering());
+
+  // Confirm that metadata wasn't sent to the SkiaOutputSurface if Skia was
+  // used for drawing, or confirm that the DelegatedInkPointRenderer wasn't
+  // created if platform ink is being used.
+  if (GetParam() == DelegatedInkType::kPlatformInk)
+    EXPECT_FALSE(ink_renderer());
+  else
+    EXPECT_FALSE(skia_output_surface_->last_delegated_ink_metadata());
+}
+
 }  // namespace viz
diff --git a/components/viz/service/display/output_surface_frame.h b/components/viz/service/display/output_surface_frame.h
index 8b57fd8..ae73f03 100644
--- a/components/viz/service/display/output_surface_frame.h
+++ b/components/viz/service/display/output_surface_frame.h
@@ -5,11 +5,13 @@
 #ifndef COMPONENTS_VIZ_SERVICE_DISPLAY_OUTPUT_SURFACE_FRAME_H_
 #define COMPONENTS_VIZ_SERVICE_DISPLAY_OUTPUT_SURFACE_FRAME_H_
 
+#include <memory>
 #include <vector>
 
 #include "base/macros.h"
 #include "base/optional.h"
 #include "components/viz/service/viz_service_export.h"
+#include "ui/gfx/delegated_ink_metadata.h"
 #include "ui/gfx/geometry/rect.h"
 #include "ui/gfx/geometry/size.h"
 #include "ui/latency/latency_info.h"
@@ -35,6 +37,9 @@
   std::vector<gfx::Rect> content_bounds;
   std::vector<ui::LatencyInfo> latency_info;
   bool top_controls_visible_height_changed = false;
+  // Metadata containing information to draw a delegated ink trail using
+  // platform APIs.
+  std::unique_ptr<gfx::DelegatedInkMetadata> delegated_ink_metadata;
 
  private:
   DISALLOW_COPY_AND_ASSIGN(OutputSurfaceFrame);
diff --git a/components/viz/service/display/skia_renderer.cc b/components/viz/service/display/skia_renderer.cc
index 955284c09..9026956 100644
--- a/components/viz/service/display/skia_renderer.cc
+++ b/components/viz/service/display/skia_renderer.cc
@@ -35,6 +35,7 @@
 #include "components/viz/common/resources/platform_color.h"
 #include "components/viz/common/resources/resource_format_utils.h"
 #include "components/viz/common/skia_helper.h"
+#include "components/viz/service/display/delegated_ink_handler.h"
 #include "components/viz/service/display/delegated_ink_point_renderer_skia.h"
 #include "components/viz/service/display/display_resource_provider.h"
 #include "components/viz/service/display/output_surface.h"
@@ -794,6 +795,10 @@
   } else if (swap_buffer_rect_.IsEmpty() && allow_empty_swap_) {
     output_frame.sub_buffer_rect = gfx::Rect();
   }
+  if (delegated_ink_handler_ && !UsingSkiaForDelegatedInk()) {
+    output_frame.delegated_ink_metadata =
+        delegated_ink_handler_->TakeMetadata();
+  }
 
   skia_output_surface_->SwapBuffers(std::move(output_frame));
   swap_buffer_rect_ = gfx::Rect();
@@ -2617,7 +2622,7 @@
   // FlushBatchedQuads() call so that the trail can always be on top of
   // everything else that has already been drawn on the page. For the same
   // reason, it should only happen on the root render pass.
-  if (is_root_render_pass && delegated_ink_point_renderer_)
+  if (is_root_render_pass && UsingSkiaForDelegatedInk())
     DrawDelegatedInkTrail();
 
   base::OnceClosure on_finished_callback;
@@ -2907,22 +2912,47 @@
   return it->second.size;
 }
 
-bool SkiaRenderer::CreateDelegatedInkPointRenderer() {
-  DCHECK(!delegated_ink_point_renderer_);
-  delegated_ink_point_renderer_ =
-      std::make_unique<DelegatedInkPointRendererSkia>();
-  return true;
+void SkiaRenderer::SetDelegatedInkPointRendererSkiaForTest(
+    std::unique_ptr<DelegatedInkPointRendererSkia> renderer) {
+  DCHECK(!delegated_ink_handler_);
+  delegated_ink_handler_ = std::make_unique<DelegatedInkHandler>(
+      output_surface_->capabilities().supports_delegated_ink);
+  delegated_ink_handler_->SetDelegatedInkPointRendererForTest(
+      std::move(renderer));
 }
 
 void SkiaRenderer::DrawDelegatedInkTrail() {
-  delegated_ink_point_renderer_->DrawDelegatedInkTrail(current_canvas_);
+  if (!delegated_ink_handler_ || !delegated_ink_handler_->GetInkRenderer())
+    return;
+
+  delegated_ink_handler_->GetInkRenderer()->DrawDelegatedInkTrail(
+      current_canvas_);
 }
 
-DelegatedInkPointRendererBase* SkiaRenderer::GetDelegatedInkPointRenderer() {
-  if (!delegated_ink_point_renderer_ && !CreateDelegatedInkPointRenderer())
+DelegatedInkPointRendererBase* SkiaRenderer::GetDelegatedInkPointRenderer(
+    bool create_if_necessary) {
+  if (!delegated_ink_handler_ && !create_if_necessary)
     return nullptr;
 
-  return delegated_ink_point_renderer_.get();
+  if (!delegated_ink_handler_) {
+    delegated_ink_handler_ = std::make_unique<DelegatedInkHandler>(
+        output_surface_->capabilities().supports_delegated_ink);
+  }
+
+  return delegated_ink_handler_->GetInkRenderer();
+}
+
+void SkiaRenderer::SetDelegatedInkMetadata(
+    std::unique_ptr<gfx::DelegatedInkMetadata> metadata) {
+  if (!delegated_ink_handler_) {
+    delegated_ink_handler_ = std::make_unique<DelegatedInkHandler>(
+        output_surface_->capabilities().supports_delegated_ink);
+  }
+  delegated_ink_handler_->SetDelegatedInkMetadata(std::move(metadata));
+}
+
+bool SkiaRenderer::UsingSkiaForDelegatedInk() const {
+  return delegated_ink_handler_ && delegated_ink_handler_->GetInkRenderer();
 }
 
 #if defined(OS_APPLE)
diff --git a/components/viz/service/display/skia_renderer.h b/components/viz/service/display/skia_renderer.h
index 2dbe305..3f08752 100644
--- a/components/viz/service/display/skia_renderer.h
+++ b/components/viz/service/display/skia_renderer.h
@@ -27,7 +27,7 @@
 class AggregatedRenderPassDrawQuad;
 class DebugBorderDrawQuad;
 class DelegatedInkPointRendererBase;
-class DelegatedInkPointRendererSkia;
+class DelegatedInkHandler;
 class PictureDrawQuad;
 class SkiaOutputSurface;
 class SolidColorDrawQuad;
@@ -59,7 +59,10 @@
     disable_picture_quad_image_filtering_ = disable;
   }
 
-  DelegatedInkPointRendererBase* GetDelegatedInkPointRenderer() override;
+  DelegatedInkPointRendererBase* GetDelegatedInkPointRenderer(
+      bool create_if_necessary) override;
+  void SetDelegatedInkMetadata(
+      std::unique_ptr<gfx::DelegatedInkMetadata> metadata) override;
 
  protected:
   bool CanPartialSwap() override;
@@ -91,9 +94,10 @@
   void DidChangeVisibility() override;
   void FinishDrawingQuadList() override;
   void GenerateMipmap() override;
-  bool CreateDelegatedInkPointRenderer() override;
+  void SetDelegatedInkPointRendererSkiaForTest(
+      std::unique_ptr<DelegatedInkPointRendererSkia> renderer) override;
 
-  std::unique_ptr<DelegatedInkPointRendererSkia> delegated_ink_point_renderer_;
+  std::unique_ptr<DelegatedInkHandler> delegated_ink_handler_;
 
  private:
   enum class BypassMode;
@@ -347,6 +351,8 @@
                  base::flat_map<gfx::ColorSpace, sk_sp<SkRuntimeEffect>>>
       color_filter_cache_;
 
+  bool UsingSkiaForDelegatedInk() const;
+
   DISALLOW_COPY_AND_ASSIGN(SkiaRenderer);
 };
 
diff --git a/components/viz/service/display_embedder/skia_output_surface_impl_on_gpu.cc b/components/viz/service/display_embedder/skia_output_surface_impl_on_gpu.cc
index 97cbaf0..c9a1e80 100644
--- a/components/viz/service/display_embedder/skia_output_surface_impl_on_gpu.cc
+++ b/components/viz/service/display_embedder/skia_output_surface_impl_on_gpu.cc
@@ -1571,6 +1571,11 @@
   if (context_is_lost_)
     return;
 
+  if (gl_surface_ && frame && frame->delegated_ink_metadata) {
+    gl_surface_->SetDelegatedInkTrailStartPoint(
+        std::move(frame->delegated_ink_metadata));
+  }
+
   bool sync_cpu =
       gpu::ShouldVulkanSyncCpuForSkiaSubmit(vulkan_context_provider_);
 
diff --git a/components/viz/service/frame_sinks/root_compositor_frame_sink_impl.cc b/components/viz/service/frame_sinks/root_compositor_frame_sink_impl.cc
index 7aa82a0..7d804bdc 100644
--- a/components/viz/service/frame_sinks/root_compositor_frame_sink_impl.cc
+++ b/components/viz/service/frame_sinks/root_compositor_frame_sink_impl.cc
@@ -322,8 +322,10 @@
 
 void RootCompositorFrameSinkImpl::SetDelegatedInkPointRenderer(
     mojo::PendingReceiver<mojom::DelegatedInkPointRenderer> receiver) {
-  if (auto* ink_renderer = display_->GetDelegatedInkPointRenderer())
+  if (auto* ink_renderer = display_->GetDelegatedInkPointRenderer(
+          /*create_if_necessary=*/true)) {
     ink_renderer->InitMessagePipeline(std::move(receiver));
+  }
 }
 
 void RootCompositorFrameSinkImpl::SetNeedsBeginFrame(bool needs_begin_frame) {
diff --git a/components/viz/test/BUILD.gn b/components/viz/test/BUILD.gn
index 94efd470..e7904cf 100644
--- a/components/viz/test/BUILD.gn
+++ b/components/viz/test/BUILD.gn
@@ -26,6 +26,8 @@
     "begin_frame_source_test.h",
     "compositor_frame_helpers.cc",
     "compositor_frame_helpers.h",
+    "delegated_ink_point_renderer_skia_for_test.cc",
+    "delegated_ink_point_renderer_skia_for_test.h",
     "fake_compositor_frame_sink_client.cc",
     "fake_compositor_frame_sink_client.h",
     "fake_delay_based_time_source.cc",
diff --git a/components/viz/test/compositor_frame_helpers.cc b/components/viz/test/compositor_frame_helpers.cc
index b87e3fa..17dc2ab 100644
--- a/components/viz/test/compositor_frame_helpers.cc
+++ b/components/viz/test/compositor_frame_helpers.cc
@@ -125,6 +125,13 @@
   return *this;
 }
 
+CompositorFrameBuilder& CompositorFrameBuilder::AddDelegatedInkMetadata(
+    const gfx::DelegatedInkMetadata& metadata) {
+  frame_->metadata.delegated_ink_metadata =
+      std::make_unique<gfx::DelegatedInkMetadata>(metadata);
+  return *this;
+}
+
 CompositorFrame CompositorFrameBuilder::MakeInitCompositorFrame() const {
   static FrameTokenGenerator next_token;
   CompositorFrame frame;
diff --git a/components/viz/test/compositor_frame_helpers.h b/components/viz/test/compositor_frame_helpers.h
index 1dadaca..49c080c 100644
--- a/components/viz/test/compositor_frame_helpers.h
+++ b/components/viz/test/compositor_frame_helpers.h
@@ -63,6 +63,9 @@
   CompositorFrameBuilder& SetDeadline(const FrameDeadline& deadline);
   CompositorFrameBuilder& SetSendFrameTokenToEmbedder(bool send);
 
+  CompositorFrameBuilder& AddDelegatedInkMetadata(
+      const gfx::DelegatedInkMetadata& metadata);
+
  private:
   CompositorFrame MakeInitCompositorFrame() const;
 
diff --git a/components/viz/test/delegated_ink_point_renderer_skia_for_test.cc b/components/viz/test/delegated_ink_point_renderer_skia_for_test.cc
new file mode 100644
index 0000000..5ddf4a7
--- /dev/null
+++ b/components/viz/test/delegated_ink_point_renderer_skia_for_test.cc
@@ -0,0 +1,25 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "components/viz/test/delegated_ink_point_renderer_skia_for_test.h"
+
+#include <utility>
+
+#include "ui/gfx/delegated_ink_metadata.h"
+
+namespace viz {
+
+DelegatedInkPointRendererSkiaForTest::DelegatedInkPointRendererSkiaForTest() =
+    default;
+
+DelegatedInkPointRendererSkiaForTest::~DelegatedInkPointRendererSkiaForTest() =
+    default;
+
+void DelegatedInkPointRendererSkiaForTest::SetDelegatedInkMetadata(
+    std::unique_ptr<gfx::DelegatedInkMetadata> metadata) {
+  last_metadata_ = std::make_unique<gfx::DelegatedInkMetadata>(*metadata.get());
+  DelegatedInkPointRendererBase::SetDelegatedInkMetadata(std::move(metadata));
+}
+
+}  // namespace viz
diff --git a/components/viz/test/delegated_ink_point_renderer_skia_for_test.h b/components/viz/test/delegated_ink_point_renderer_skia_for_test.h
new file mode 100644
index 0000000..ac0e3a04
--- /dev/null
+++ b/components/viz/test/delegated_ink_point_renderer_skia_for_test.h
@@ -0,0 +1,35 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef COMPONENTS_VIZ_TEST_DELEGATED_INK_POINT_RENDERER_SKIA_FOR_TEST_H_
+#define COMPONENTS_VIZ_TEST_DELEGATED_INK_POINT_RENDERER_SKIA_FOR_TEST_H_
+
+#include <memory>
+
+#include "components/viz/service/display/delegated_ink_point_renderer_skia.h"
+
+namespace viz {
+
+// Test class that just holds on to the most recently received metadata,
+// specifically for testing purposes.
+class DelegatedInkPointRendererSkiaForTest
+    : public DelegatedInkPointRendererSkia {
+ public:
+  DelegatedInkPointRendererSkiaForTest();
+  ~DelegatedInkPointRendererSkiaForTest() override;
+
+  void SetDelegatedInkMetadata(
+      std::unique_ptr<gfx::DelegatedInkMetadata> metadata) override;
+
+  const gfx::DelegatedInkMetadata* last_metadata() const {
+    return last_metadata_.get();
+  }
+
+ private:
+  std::unique_ptr<gfx::DelegatedInkMetadata> last_metadata_;
+};
+
+}  // namespace viz
+
+#endif  // COMPONENTS_VIZ_TEST_DELEGATED_INK_POINT_RENDERER_SKIA_FOR_TEST_H_
diff --git a/components/viz/test/fake_skia_output_surface.cc b/components/viz/test/fake_skia_output_surface.cc
index 9646a13..20e168f 100644
--- a/components/viz/test/fake_skia_output_surface.cc
+++ b/components/viz/test/fake_skia_output_surface.cc
@@ -79,6 +79,8 @@
 
 void FakeSkiaOutputSurface::SwapBuffers(OutputSurfaceFrame frame) {
   DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
+  if (frame.delegated_ink_metadata)
+    last_delegated_ink_metadata_ = std::move(frame.delegated_ink_metadata);
   base::ThreadTaskRunnerHandle::Get()->PostTask(
       FROM_HERE, base::BindOnce(&FakeSkiaOutputSurface::SwapBuffersAck,
                                 weak_ptr_factory_.GetWeakPtr()));
diff --git a/components/viz/test/fake_skia_output_surface.h b/components/viz/test/fake_skia_output_surface.h
index 2cba17f..44926ec 100644
--- a/components/viz/test/fake_skia_output_surface.h
+++ b/components/viz/test/fake_skia_output_surface.h
@@ -133,6 +133,14 @@
       base::OnceClosure callback,
       std::vector<gpu::SyncToken> sync_tokens) override;
 
+  void UsePlatformDelegatedInkForTesting() {
+    capabilities_.supports_delegated_ink = true;
+  }
+
+  gfx::DelegatedInkMetadata* last_delegated_ink_metadata() const {
+    return last_delegated_ink_metadata_.get();
+  }
+
  private:
   explicit FakeSkiaOutputSurface(
       scoped_refptr<ContextProvider> context_provider);
@@ -155,6 +163,9 @@
   // SkSurfaces for render passes, sk_surfaces_[0] is the root surface.
   base::flat_map<AggregatedRenderPassId, sk_sp<SkSurface>> sk_surfaces_;
 
+  // Most recent delegated ink metadata to have arrived via a SwapBuffers call.
+  std::unique_ptr<gfx::DelegatedInkMetadata> last_delegated_ink_metadata_;
+
   THREAD_CHECKER(thread_checker_);
 
   base::WeakPtrFactory<FakeSkiaOutputSurface> weak_ptr_factory_{this};
diff --git a/docs/updating_clang_format_binaries.md b/docs/updating_clang_format_binaries.md
index 8a79686..a214706 100644
--- a/docs/updating_clang_format_binaries.md
+++ b/docs/updating_clang_format_binaries.md
@@ -60,19 +60,27 @@
 cd build
 
 # On Mac, do the following:
-MACOSX_DEPLOYMENT_TARGET=10.9 cmake -G Ninja -DCMAKE_BUILD_TYPE=Release \
+MACOSX_DEPLOYMENT_TARGET=10.9 cmake -G Ninja \
+    -DCMAKE_BUILD_TYPE=Release \
     -DLLVM_ENABLE_PROJECTS=clang \
-    -DLLVM_ENABLE_ASSERTIONS=NO -DLLVM_ENABLE_THREADS=NO ../llvm/
+    -DLLVM_ENABLE_ASSERTIONS=NO \
+    -DLLVM_ENABLE_THREADS=NO \
+    '-DCMAKE_OSX_ARCHITECTURES=arm64;x86_64' \
+    ../llvm/
 
 # On Linux, do the following:
 # Note the relative paths that point to your local Chromium checkout.
-cmake -G Ninja -DCMAKE_BUILD_TYPE=Release \
+cmake -G Ninja \
+    -DCMAKE_BUILD_TYPE=Release \
     -DLLVM_ENABLE_PROJECTS=clang \
-    -DLLVM_ENABLE_ASSERTIONS=NO -DLLVM_ENABLE_THREADS=NO \
+    -DLLVM_ENABLE_ASSERTIONS=NO \
+    -DLLVM_ENABLE_THREADS=NO \
     -DCMAKE_C_COMPILER=$PWD/../../chromium/src/third_party/llvm-build/Release+Asserts/bin/clang \
     -DCMAKE_CXX_COMPILER=$PWD/../../chromium/src/third_party/llvm-build/Release+Asserts/bin/clang++ \
     -DCMAKE_ASM_COMPILER=$PWD/../../chromium/src/third_party/llvm-build/Release+Asserts/bin/clang \
-    -DLLVM_ENABLE_TERMINFO=OFF -DCMAKE_CXX_STANDARD_LIBRARIES="-static-libgcc -static-libstdc++" ../llvm/
+    -DLLVM_ENABLE_TERMINFO=OFF \
+    -DCMAKE_CXX_STANDARD_LIBRARIES="-static-libgcc -static-libstdc++" \
+    ../llvm/
 
 # Finally, build the actual clang-format binary with Ninja
 ninja clang-format
@@ -95,7 +103,7 @@
 
 Copy the binaries into your chromium checkout (under
 `src/buildtools/(win|linux64|mac)/clang-format(.exe?)`). For each binary, you'll
-need to run upload_to_google_storage.py according to the instructions in
+need to run `upload_to_google_storage.py` according to the instructions in
 [README.txt](https://chromium.googlesource.com/chromium/src/+/master/buildtools/clang_format/README.txt).
 This will upload the binary into a publicly accessible google storage bucket,
 and update `.sha1` file in your Chrome checkout. You'll check in the `.sha1`
@@ -106,7 +114,7 @@
 
 There are some auxiliary scripts that ought to be kept updated in lockstep with
 the clang-format binary. These get copied into
-third_party/clang_format/scripts in your Chromium checkout.
+`buildtools/clang_format/script` in your Chromium checkout.
 
 The `README.chromium` file ought to be updated with version and date info.
 
diff --git a/infra/config/generated/tricium-prod.cfg b/infra/config/generated/tricium-prod.cfg
index 3f34a03..e574547 100644
--- a/infra/config/generated/tricium-prod.cfg
+++ b/infra/config/generated/tricium-prod.cfg
@@ -1,6 +1,8 @@
 # Schema for this config file: ProjectConfig in
 # luci-config.appspot.com/schemas/projects:tricium-prod.cfg
 
+# This ACL setting only determines who can directly make requests;
+# it doesn't limit whose CLs will get automatic analysis run.
 acls {
   role: REQUESTER
   group: "tricium-chromium-requesters"
@@ -17,24 +19,14 @@
   path_filters: "*.h"
   owner: "gbiv@chromium.org"
   monorail_component: "Infra>Platform>Tricium>Analyzer"
-
   impls {
     runtime_platform: LINUX
     provides_for_platform: LINUX
-
     recipe {
       project: "chromium"
       bucket: "try"
       builder: "linux-clang-tidy-rel"
     }
-
-    # 2700sec == 45min. It's generally expected to take <10mins to get results
-    # for reasonably-sized CLs, but the tidy bots:
-    # - can 'only' run on 8 files at a time (where 1 'average' file is expected
-    #   to take 1-2 minutes or so of CPU).
-    # - in the case of a bug or otherwise pathological file, the bots time out
-    #   individual clang-tidy invocations after 30 minutes.
-    deadline: 2700
   }
 }
 
@@ -55,7 +47,6 @@
       bucket: "try"
       builder: "tricium-metrics-analysis"
     }
-    deadline: 900
   }
 }
 
@@ -78,7 +69,6 @@
       bucket: "try"
       builder: "tricium-oilpan-analysis"
     }
-    deadline: 900
   }
 }
 
@@ -98,7 +88,6 @@
       bucket: "try"
       builder: "tricium-simple"
     }
-    deadline: 900
   }
 }
 
@@ -128,8 +117,10 @@
     project: "chromium/src"
     git_url: "https://chromium.googlesource.com/chromium/src"
   }
+  # This group determines whose CLs will have Tricium runs automatically
+  # created. For security reasons, Tricium doesn't create runs for users that
+  # don't have tryjob access.
   whitelisted_group: "project-chromium-tryjob-access"
 }
 
 service_account: "tricium-prod@appspot.gserviceaccount.com"
-swarming_service_account: "swarming@tricium-prod.iam.gserviceaccount.com"
diff --git a/infra/config/tricium-prod.cfg b/infra/config/tricium-prod.cfg
index 3f34a03..e574547 100644
--- a/infra/config/tricium-prod.cfg
+++ b/infra/config/tricium-prod.cfg
@@ -1,6 +1,8 @@
 # Schema for this config file: ProjectConfig in
 # luci-config.appspot.com/schemas/projects:tricium-prod.cfg
 
+# This ACL setting only determines who can directly make requests;
+# it doesn't limit whose CLs will get automatic analysis run.
 acls {
   role: REQUESTER
   group: "tricium-chromium-requesters"
@@ -17,24 +19,14 @@
   path_filters: "*.h"
   owner: "gbiv@chromium.org"
   monorail_component: "Infra>Platform>Tricium>Analyzer"
-
   impls {
     runtime_platform: LINUX
     provides_for_platform: LINUX
-
     recipe {
       project: "chromium"
       bucket: "try"
       builder: "linux-clang-tidy-rel"
     }
-
-    # 2700sec == 45min. It's generally expected to take <10mins to get results
-    # for reasonably-sized CLs, but the tidy bots:
-    # - can 'only' run on 8 files at a time (where 1 'average' file is expected
-    #   to take 1-2 minutes or so of CPU).
-    # - in the case of a bug or otherwise pathological file, the bots time out
-    #   individual clang-tidy invocations after 30 minutes.
-    deadline: 2700
   }
 }
 
@@ -55,7 +47,6 @@
       bucket: "try"
       builder: "tricium-metrics-analysis"
     }
-    deadline: 900
   }
 }
 
@@ -78,7 +69,6 @@
       bucket: "try"
       builder: "tricium-oilpan-analysis"
     }
-    deadline: 900
   }
 }
 
@@ -98,7 +88,6 @@
       bucket: "try"
       builder: "tricium-simple"
     }
-    deadline: 900
   }
 }
 
@@ -128,8 +117,10 @@
     project: "chromium/src"
     git_url: "https://chromium.googlesource.com/chromium/src"
   }
+  # This group determines whose CLs will have Tricium runs automatically
+  # created. For security reasons, Tricium doesn't create runs for users that
+  # don't have tryjob access.
   whitelisted_group: "project-chromium-tryjob-access"
 }
 
 service_account: "tricium-prod@appspot.gserviceaccount.com"
-swarming_service_account: "swarming@tricium-prod.iam.gserviceaccount.com"
diff --git a/ios/google_internal/frameworks/chrome_internal_dynamic_framework.arm64.zip.sha1 b/ios/google_internal/frameworks/chrome_internal_dynamic_framework.arm64.zip.sha1
index bc68075..c0c5ca9 100644
--- a/ios/google_internal/frameworks/chrome_internal_dynamic_framework.arm64.zip.sha1
+++ b/ios/google_internal/frameworks/chrome_internal_dynamic_framework.arm64.zip.sha1
@@ -1 +1 @@
-7f51d7d7a7d415b9a5b63473bede92398f939f3a
\ No newline at end of file
+b8493e8af7e304f4041da41db7cf46a92553dc30
\ No newline at end of file
diff --git a/ios/google_internal/frameworks/chrome_internal_dynamic_framework.x64.zip.sha1 b/ios/google_internal/frameworks/chrome_internal_dynamic_framework.x64.zip.sha1
index b9de498..c03f1bda 100644
--- a/ios/google_internal/frameworks/chrome_internal_dynamic_framework.x64.zip.sha1
+++ b/ios/google_internal/frameworks/chrome_internal_dynamic_framework.x64.zip.sha1
@@ -1 +1 @@
-dfabaa8d8a5fd68622dfbc13d56722f3fc283df5
\ No newline at end of file
+2a14fd68c36337d96eb6af847b59336bc76fa611
\ No newline at end of file
diff --git a/ios/google_internal/frameworks/remoting_dogfood_internal_dynamic_framework.arm64.zip.sha1 b/ios/google_internal/frameworks/remoting_dogfood_internal_dynamic_framework.arm64.zip.sha1
index 3d8fdf3..a7c1c8a 100644
--- a/ios/google_internal/frameworks/remoting_dogfood_internal_dynamic_framework.arm64.zip.sha1
+++ b/ios/google_internal/frameworks/remoting_dogfood_internal_dynamic_framework.arm64.zip.sha1
@@ -1 +1 @@
-1ec3e0950c657add267ec110b9891eac6299e1a5
\ No newline at end of file
+4dca2eef4c17e56e5bef6faac90ef4e07dc23795
\ No newline at end of file
diff --git a/ios/google_internal/frameworks/remoting_dogfood_internal_dynamic_framework.x64.zip.sha1 b/ios/google_internal/frameworks/remoting_dogfood_internal_dynamic_framework.x64.zip.sha1
index 404149ff..2eaf655e 100644
--- a/ios/google_internal/frameworks/remoting_dogfood_internal_dynamic_framework.x64.zip.sha1
+++ b/ios/google_internal/frameworks/remoting_dogfood_internal_dynamic_framework.x64.zip.sha1
@@ -1 +1 @@
-04f924c729125a1a62f51eeb9fbcdc13fed19966
\ No newline at end of file
+0e0d9d59b4a72f36750f8f8024be264e9974b4c7
\ No newline at end of file
diff --git a/ios/google_internal/frameworks/remoting_internal_dynamic_framework.arm64.zip.sha1 b/ios/google_internal/frameworks/remoting_internal_dynamic_framework.arm64.zip.sha1
index e504739..63537a2 100644
--- a/ios/google_internal/frameworks/remoting_internal_dynamic_framework.arm64.zip.sha1
+++ b/ios/google_internal/frameworks/remoting_internal_dynamic_framework.arm64.zip.sha1
@@ -1 +1 @@
-54d9f65ed4ee86aa1ebe84b59a51f8b877426b5a
\ No newline at end of file
+4bb22c31447f6234cdab1e76c7a15ba09b8043b2
\ No newline at end of file
diff --git a/ios/google_internal/frameworks/remoting_internal_dynamic_framework.x64.zip.sha1 b/ios/google_internal/frameworks/remoting_internal_dynamic_framework.x64.zip.sha1
index 9423f96..0dd299b6 100644
--- a/ios/google_internal/frameworks/remoting_internal_dynamic_framework.x64.zip.sha1
+++ b/ios/google_internal/frameworks/remoting_internal_dynamic_framework.x64.zip.sha1
@@ -1 +1 @@
-504b09535952d8cb9639bf4643353094147550f0
\ No newline at end of file
+2fca7009b725aa7f6682e9c141d89304be675b52
\ No newline at end of file
diff --git a/ios/google_internal/frameworks/web_view_shell_internal_dynamic_framework.arm64.zip.sha1 b/ios/google_internal/frameworks/web_view_shell_internal_dynamic_framework.arm64.zip.sha1
index c900a00..335269b 100644
--- a/ios/google_internal/frameworks/web_view_shell_internal_dynamic_framework.arm64.zip.sha1
+++ b/ios/google_internal/frameworks/web_view_shell_internal_dynamic_framework.arm64.zip.sha1
@@ -1 +1 @@
-b3ce9b84880be7feff4a89f318c064e073abb932
\ No newline at end of file
+f9a3602b252fd607d41119a37d9cc0f286a89f88
\ No newline at end of file
diff --git a/ios/google_internal/frameworks/web_view_shell_internal_dynamic_framework.x64.zip.sha1 b/ios/google_internal/frameworks/web_view_shell_internal_dynamic_framework.x64.zip.sha1
index 468e3fa8c..c1f0fdb 100644
--- a/ios/google_internal/frameworks/web_view_shell_internal_dynamic_framework.x64.zip.sha1
+++ b/ios/google_internal/frameworks/web_view_shell_internal_dynamic_framework.x64.zip.sha1
@@ -1 +1 @@
-d4baa1ce528696851c21bfc0b9871fa9fb644325
\ No newline at end of file
+584bec3f219834b63d887c3c443456b7fa630733
\ No newline at end of file
diff --git a/media/capture/BUILD.gn b/media/capture/BUILD.gn
index 81f0559..f070c07d 100644
--- a/media/capture/BUILD.gn
+++ b/media/capture/BUILD.gn
@@ -525,6 +525,7 @@
       "mfplat.lib",
       "mfreadwrite.lib",
       "mfuuid.lib",
+      "dxguid.lib",
     ]
     ldflags = [
       "/DELAYLOAD:mf.dll",
diff --git a/media/capture/video/win/d3d_capture_test_utils.cc b/media/capture/video/win/d3d_capture_test_utils.cc
index f9139da..ed752218 100644
--- a/media/capture/video/win/d3d_capture_test_utils.cc
+++ b/media/capture/video/win/d3d_capture_test_utils.cc
@@ -504,10 +504,11 @@
     const D3D11_TEXTURE2D_DESC* desc,
     const D3D11_SUBRESOURCE_DATA* initial_data,
     ID3D11Texture2D** texture2D) {
-  OnCreateTexture2D(desc, initial_data, texture2D);
   Microsoft::WRL::ComPtr<MockD3D11Texture2D> mock_texture(
       new MockD3D11Texture2D());
-  return mock_texture.CopyTo(IID_PPV_ARGS(texture2D));
+  HRESULT hr = mock_texture.CopyTo(IID_PPV_ARGS(texture2D));
+  OnCreateTexture2D(desc, initial_data, texture2D);
+  return hr;
 }
 
 IFACEMETHODIMP MockD3D11Device::CreateTexture3D(
@@ -804,6 +805,13 @@
   ON_CALL(*mock_immediate_context_.Get(), OnMap)
       .WillByDefault([](ID3D11Resource*, UINT, D3D11_MAP, UINT,
                         D3D11_MAPPED_SUBRESOURCE*) { return E_NOTIMPL; });
+  ON_CALL(*this, OnCreateTexture2D)
+      .WillByDefault([](const D3D11_TEXTURE2D_DESC*,
+                        const D3D11_SUBRESOURCE_DATA*,
+                        ID3D11Texture2D** texture) {
+        static_cast<MockD3D11Texture2D*>(*texture)->SetupDefaultMocks();
+        return S_OK;
+      });
 }
 
 IFACEMETHODIMP MockDXGIResource::CreateSubresourceSurface(
@@ -908,16 +916,20 @@
                                                   void* data) {
   return E_NOTIMPL;
 }
-IFACEMETHODIMP MockD3D11Texture2D::SetPrivateData(REFGUID guid,
-                                                  UINT data_size,
-                                                  const void* data) {
-  return E_NOTIMPL;
-}
 IFACEMETHODIMP MockD3D11Texture2D::SetPrivateDataInterface(
     REFGUID guid,
     const IUnknown* data) {
   return E_NOTIMPL;
 }
+
+void MockD3D11Texture2D::SetupDefaultMocks() {
+  ON_CALL(*this, SetPrivateData(testing::_, testing::_, testing::_))
+      .WillByDefault(testing::Return(E_NOTIMPL));
+  ON_CALL(*this,
+          SetPrivateData(WKPDID_D3DDebugObjectName, testing::_, testing::_))
+      .WillByDefault(testing::Return(S_OK));
+}
+
 MockD3D11Texture2D::~MockD3D11Texture2D() {}
 
-}  // namespace media
\ No newline at end of file
+}  // namespace media
diff --git a/media/capture/video/win/d3d_capture_test_utils.h b/media/capture/video/win/d3d_capture_test_utils.h
index 08efcd4..9d4bb75 100644
--- a/media/capture/video/win/d3d_capture_test_utils.h
+++ b/media/capture/video/win/d3d_capture_test_utils.h
@@ -8,6 +8,7 @@
 #include <d3d11_4.h>
 #include <wrl.h>
 #include "base/memory/ref_counted.h"
+#include "media/base/win/test_utils.h"
 #include "testing/gmock/include/gmock/gmock.h"
 #include "testing/gtest/include/gtest/gtest.h"
 
@@ -722,9 +723,12 @@
   // ID3D11DeviceChild
   IFACEMETHODIMP_(void) GetDevice(ID3D11Device** device);
   IFACEMETHODIMP GetPrivateData(REFGUID guid, UINT* data_size, void* data);
-  IFACEMETHODIMP SetPrivateData(REFGUID guid, UINT data_size, const void* data);
+  MOCK_STDCALL_METHOD3(SetPrivateData,
+                       HRESULT(REFGUID guid, UINT data_size, const void* data));
   IFACEMETHODIMP SetPrivateDataInterface(REFGUID guid, const IUnknown* data);
 
+  void SetupDefaultMocks();
+
   Microsoft::WRL::ComPtr<MockDXGIResource> mock_resource_;
 
  private:
@@ -735,4 +739,4 @@
 
 }  // namespace media
 
-#endif  // MEDIA_CAPTURE_VIDEO_WIN_D3D_CAPTURE_TEST_UTILS_H_
\ No newline at end of file
+#endif  // MEDIA_CAPTURE_VIDEO_WIN_D3D_CAPTURE_TEST_UTILS_H_
diff --git a/media/capture/video/win/gpu_memory_buffer_tracker.cc b/media/capture/video/win/gpu_memory_buffer_tracker.cc
index 2192f5b..e39f2db 100644
--- a/media/capture/video/win/gpu_memory_buffer_tracker.cc
+++ b/media/capture/video/win/gpu_memory_buffer_tracker.cc
@@ -8,6 +8,7 @@
 #include "base/notreached.h"
 #include "base/win/scoped_handle.h"
 #include "gpu/ipc/common/dxgi_helpers.h"
+#include "media/base/win/mf_helpers.h"
 #include "media/capture/video/video_capture_buffer_handle.h"
 #include "ui/gfx/geometry/size.h"
 
@@ -41,6 +42,12 @@
                 << logging::SystemErrorCodeToString(hr);
     return base::win::ScopedHandle();
   }
+  hr = SetDebugName(d3d11_texture.Get(), "Camera_MemoryBufferTracker");
+  if (FAILED(hr)) {
+    DLOG(ERROR) << "Failed to label D3D11 texture: "
+                << logging::SystemErrorCodeToString(hr);
+    return base::win::ScopedHandle();
+  }
 
   Microsoft::WRL::ComPtr<IDXGIResource1> dxgi_resource;
   hr = d3d11_texture.As(&dxgi_resource);
diff --git a/media/capture/video/win/gpu_memory_buffer_tracker_unittest.cc b/media/capture/video/win/gpu_memory_buffer_tracker_unittest.cc
index c8dbd8ed..3cae858 100644
--- a/media/capture/video/win/gpu_memory_buffer_tracker_unittest.cc
+++ b/media/capture/video/win/gpu_memory_buffer_tracker_unittest.cc
@@ -156,6 +156,7 @@
   dxgi_device_manager_->GetMockDevice()->SetupDefaultMocks();
   std::unique_ptr<VideoCaptureBufferTracker> tracker =
       std::make_unique<GpuMemoryBufferTracker>(dxgi_device_manager_);
+
   EXPECT_EQ(tracker->Init(expected_buffer_size, PIXEL_FORMAT_NV12, nullptr),
             true);
 
@@ -267,4 +268,4 @@
   EXPECT_TRUE(memory_region.IsValid());
 }
 
-}  // namespace media
\ No newline at end of file
+}  // namespace media
diff --git a/net/base/features.cc b/net/base/features.cc
index 031bd59f..4235fe35 100644
--- a/net/base/features.cc
+++ b/net/base/features.cc
@@ -119,6 +119,10 @@
 
 const base::Feature kPostQuantumCECPQ2{"PostQuantumCECPQ2",
                                        base::FEATURE_DISABLED_BY_DEFAULT};
+const base::Feature kPostQuantumCECPQ2SomeDomains{
+    "PostQuantumCECPQ2SomeDomains", base::FEATURE_ENABLED_BY_DEFAULT};
+const base::FeatureParam<std::string>
+    kPostQuantumCECPQ2Prefix(&kPostQuantumCECPQ2SomeDomains, "prefix", "aa");
 
 const base::Feature kNetUnusedIdleSocketTimeout{
     "NetUnusedIdleSocketTimeout", base::FEATURE_DISABLED_BY_DEFAULT};
diff --git a/net/base/features.h b/net/base/features.h
index 63a6cff..6639d5f 100644
--- a/net/base/features.h
+++ b/net/base/features.h
@@ -178,6 +178,13 @@
 // Enables CECPQ2, a post-quantum key-agreement, in TLS 1.3 connections.
 NET_EXPORT extern const base::Feature kPostQuantumCECPQ2;
 
+// Enables CECPQ2, a post-quantum key-agreement, in TLS 1.3 connections for a
+// subset of domains. (This is intended as Finch kill-switch. For testing
+// compatibility with large ClientHello messages, use |kPostQuantumCECPQ2|.)
+NET_EXPORT extern const base::Feature kPostQuantumCECPQ2SomeDomains;
+NET_EXPORT extern const base::FeatureParam<std::string>
+    kPostQuantumCECPQ2Prefix;
+
 // Changes the timeout after which unused sockets idle sockets are cleaned up.
 NET_EXPORT extern const base::Feature kNetUnusedIdleSocketTimeout;
 
diff --git a/net/socket/ssl_client_socket_impl.cc b/net/socket/ssl_client_socket_impl.cc
index e24e3fe..9c805c087 100644
--- a/net/socket/ssl_client_socket_impl.cc
+++ b/net/socket/ssl_client_socket_impl.cc
@@ -38,6 +38,7 @@
 #include "net/base/features.h"
 #include "net/base/ip_address.h"
 #include "net/base/net_errors.h"
+#include "net/base/registry_controlled_domains/registry_controlled_domain.h"
 #include "net/base/trace_constants.h"
 #include "net/base/url_util.h"
 #include "net/cert/cert_verifier.h"
@@ -237,6 +238,18 @@
                                 : RSAKeyUsage::kMissingDigitalSignature;
 }
 
+// IsCECPQ2Host returns true if the given host is eligible for CECPQ2. This is
+// used to implement a gradual rollout as the larger TLS messages may cause
+// middlebox issues.
+bool IsCECPQ2Host(const std::string& host) {
+  // Currently only eTLD+1s that start with "aa" are included, for example
+  // aardvark.com or aaron.com.
+  return registry_controlled_domains::GetDomainAndRegistry(
+             host, registry_controlled_domains::PrivateRegistryFilter::
+                       EXCLUDE_PRIVATE_REGISTRIES)
+             .find(features::kPostQuantumCECPQ2Prefix.Get()) == 0;
+}
+
 }  // namespace
 
 class SSLClientSocketImpl::SSLContext {
@@ -297,12 +310,6 @@
     SSL_CTX_set_msg_callback(ssl_ctx_.get(), MessageCallback);
 
     ConfigureCertificateCompression(ssl_ctx_.get());
-
-    if (base::FeatureList::IsEnabled(features::kPostQuantumCECPQ2)) {
-      static const int kCurves[] = {NID_CECPQ2, NID_X25519,
-                                    NID_X9_62_prime256v1, NID_secp384r1};
-      SSL_CTX_set1_curves(ssl_ctx_.get(), kCurves, base::size(kCurves));
-    }
   }
 
   static int ClientCertRequestCallback(SSL* ssl, void* arg) {
@@ -752,17 +759,32 @@
   if (!ssl_ || !context->SetClientSocketForSSL(ssl_.get(), this))
     return ERR_UNEXPECTED;
 
+  IPAddress unused;
+  const bool host_is_ip_address =
+      unused.AssignFromIPLiteral(host_and_port_.host());
+
   // SNI should only contain valid DNS hostnames, not IP addresses (see RFC
   // 6066, Section 3).
   //
   // TODO(rsleevi): Should this code allow hostnames that violate the LDH rule?
   // See https://crbug.com/496472 and https://crbug.com/496468 for discussion.
-  IPAddress unused;
-  if (!unused.AssignFromIPLiteral(host_and_port_.host()) &&
+  if (!host_is_ip_address &&
       !SSL_set_tlsext_host_name(ssl_.get(), host_and_port_.host().c_str())) {
     return ERR_UNEXPECTED;
   }
 
+  if (context_->config().cecpq2_enabled &&
+      (base::FeatureList::IsEnabled(features::kPostQuantumCECPQ2) ||
+       (!host_is_ip_address &&
+        base::FeatureList::IsEnabled(features::kPostQuantumCECPQ2SomeDomains) &&
+        IsCECPQ2Host(host_and_port_.host())))) {
+    static const int kCurves[] = {NID_CECPQ2, NID_X25519, NID_X9_62_prime256v1,
+                                  NID_secp384r1};
+    if (!SSL_set1_curves(ssl_.get(), kCurves, base::size(kCurves))) {
+      return ERR_UNEXPECTED;
+    }
+  }
+
   if (IsCachingEnabled()) {
     bssl::UniquePtr<SSL_SESSION> session =
         context_->ssl_client_session_cache()->Lookup(
diff --git a/net/socket/ssl_server_socket_impl.cc b/net/socket/ssl_server_socket_impl.cc
index 53bff25..5a8a69ed 100644
--- a/net/socket/ssl_server_socket_impl.cc
+++ b/net/socket/ssl_server_socket_impl.cc
@@ -855,6 +855,12 @@
     CHECK(SSL_set_signing_algorithm_prefs(ssl_.get(), &id, 1));
   }
 
+  const std::vector<int>& curves =
+      context_->ssl_server_config_.curves_for_testing;
+  if (!curves.empty()) {
+    CHECK(SSL_set1_curves(ssl_.get(), curves.data(), curves.size()));
+  }
+
   transport_adapter_.reset(new SocketBIOAdapter(
       transport_socket_.get(), kBufferSize, kBufferSize, this));
   BIO* transport_bio = transport_adapter_->bio();
diff --git a/net/ssl/ssl_config_service.cc b/net/ssl/ssl_config_service.cc
index c45f70d3..eaacabd 100644
--- a/net/ssl/ssl_config_service.cc
+++ b/net/ssl/ssl_config_service.cc
@@ -17,9 +17,11 @@
 bool SSLContextConfigsAreEqual(const net::SSLContextConfig& config1,
                                const net::SSLContextConfig& config2) {
   return std::tie(config1.version_min, config1.version_min_warn,
-                  config1.version_max, config1.disabled_cipher_suites) ==
+                  config1.version_max, config1.disabled_cipher_suites,
+                  config1.cecpq2_enabled) ==
          std::tie(config2.version_min, config2.version_min_warn,
-                  config2.version_max, config2.disabled_cipher_suites);
+                  config2.version_max, config2.disabled_cipher_suites,
+                  config2.cecpq2_enabled);
 }
 
 }  // namespace
diff --git a/net/ssl/ssl_config_service.h b/net/ssl/ssl_config_service.h
index 8fc5cbf7..070a52b 100644
--- a/net/ssl/ssl_config_service.h
+++ b/net/ssl/ssl_config_service.h
@@ -43,6 +43,11 @@
   // Ex: To disable TLS_RSA_WITH_RC4_128_MD5, specify 0x0004, while to
   // disable TLS_ECDH_ECDSA_WITH_RC4_128_SHA, specify 0xC002.
   std::vector<uint16_t> disabled_cipher_suites;
+
+  // If false, disables post-quantum key agreement in TLS connections.
+  bool cecpq2_enabled = true;
+
+  // ADDING MORE HERE? Don't forget to update |SSLContextConfigsAreEqual|.
 };
 
 // The interface for retrieving global SSL configuration.  This interface
diff --git a/net/ssl/ssl_server_config.h b/net/ssl/ssl_server_config.h
index 4a00fd2..4384c0cf 100644
--- a/net/ssl/ssl_server_config.h
+++ b/net/ssl/ssl_server_config.h
@@ -79,6 +79,10 @@
   // used in unit tests.
   base::Optional<uint16_t> signature_algorithm_for_testing;
 
+  // curves_for_testing, if not empty, specifies the list of NID values (e.g.
+  // NID_X25519) to configure as supported curves for the TLS connection.
+  std::vector<int> curves_for_testing;
+
   // Sets the requirement for client certificates during handshake.
   ClientCertType client_cert_type;
 
diff --git a/services/network/public/mojom/ssl_config.mojom b/services/network/public/mojom/ssl_config.mojom
index c2b636e9..48098ab 100644
--- a/services/network/public/mojom/ssl_config.mojom
+++ b/services/network/public/mojom/ssl_config.mojom
@@ -43,6 +43,9 @@
   // ".example.net" matches exactly "example.net". Hostnames must be
   // canonicalized according to the rules used by GURL.
   array<string> client_cert_pooling_policy;
+
+  // If false, disables post-quantum key agreement in TLS connections.
+  bool cecpq2_enabled = true;
 };
 
 // Receives SSL configuration updates.
diff --git a/services/network/ssl_config_type_converter.cc b/services/network/ssl_config_type_converter.cc
index 7d09bb5..4b5daac 100644
--- a/services/network/ssl_config_type_converter.cc
+++ b/services/network/ssl_config_type_converter.cc
@@ -37,6 +37,7 @@
   DCHECK_LE(net_config.version_min, net_config.version_max);
 
   net_config.disabled_cipher_suites = mojo_config->disabled_cipher_suites;
+  net_config.cecpq2_enabled = mojo_config->cecpq2_enabled;
   return net_config;
 }
 
diff --git a/testing/buildbot/chromium.android.fyi.json b/testing/buildbot/chromium.android.fyi.json
index 817fa113..8c822762 100644
--- a/testing/buildbot/chromium.android.fyi.json
+++ b/testing/buildbot/chromium.android.fyi.json
@@ -4534,11 +4534,11 @@
             "--bucket",
             "chromium-result-details",
             "--test-name",
-            "weblayer_instrumentation_test_versions_apk_Client Library Skew Tests For 89.0.4389.110"
+            "weblayer_instrumentation_test_versions_apk_Client Library Skew Tests For 89.0.4389.112"
           ],
           "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
         },
-        "name": "weblayer_instrumentation_test_versions_apk_Client Library Skew Tests For 89.0.4389.110",
+        "name": "weblayer_instrumentation_test_versions_apk_Client Library Skew Tests For 89.0.4389.112",
         "resultdb": {
           "enable": true
         },
@@ -4548,7 +4548,7 @@
             {
               "cipd_package": "chromium/testing/weblayer-x86",
               "location": "weblayer_instrumentation_test_M89",
-              "revision": "version:89.0.4389.110"
+              "revision": "version:89.0.4389.112"
             },
             {
               "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
@@ -4613,11 +4613,11 @@
             "--bucket",
             "chromium-result-details",
             "--test-name",
-            "weblayer_instrumentation_test_versions_apk_Client Library Skew Tests For 90.0.4430.43"
+            "weblayer_instrumentation_test_versions_apk_Client Library Skew Tests For 90.0.4430.45"
           ],
           "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
         },
-        "name": "weblayer_instrumentation_test_versions_apk_Client Library Skew Tests For 90.0.4430.43",
+        "name": "weblayer_instrumentation_test_versions_apk_Client Library Skew Tests For 90.0.4430.45",
         "resultdb": {
           "enable": true
         },
@@ -4627,7 +4627,7 @@
             {
               "cipd_package": "chromium/testing/weblayer-x86",
               "location": "weblayer_instrumentation_test_M90",
-              "revision": "version:90.0.4430.43"
+              "revision": "version:90.0.4430.45"
             },
             {
               "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
@@ -4771,11 +4771,11 @@
             "--bucket",
             "chromium-result-details",
             "--test-name",
-            "weblayer_instrumentation_test_versions_apk_Implementation Library Skew Tests For 89.0.4389.110"
+            "weblayer_instrumentation_test_versions_apk_Implementation Library Skew Tests For 89.0.4389.112"
           ],
           "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
         },
-        "name": "weblayer_instrumentation_test_versions_apk_Implementation Library Skew Tests For 89.0.4389.110",
+        "name": "weblayer_instrumentation_test_versions_apk_Implementation Library Skew Tests For 89.0.4389.112",
         "resultdb": {
           "enable": true
         },
@@ -4785,7 +4785,7 @@
             {
               "cipd_package": "chromium/testing/weblayer-x86",
               "location": "weblayer_instrumentation_test_M89",
-              "revision": "version:89.0.4389.110"
+              "revision": "version:89.0.4389.112"
             },
             {
               "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
@@ -4850,11 +4850,11 @@
             "--bucket",
             "chromium-result-details",
             "--test-name",
-            "weblayer_instrumentation_test_versions_apk_Implementation Library Skew Tests For 90.0.4430.43"
+            "weblayer_instrumentation_test_versions_apk_Implementation Library Skew Tests For 90.0.4430.45"
           ],
           "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
         },
-        "name": "weblayer_instrumentation_test_versions_apk_Implementation Library Skew Tests For 90.0.4430.43",
+        "name": "weblayer_instrumentation_test_versions_apk_Implementation Library Skew Tests For 90.0.4430.45",
         "resultdb": {
           "enable": true
         },
@@ -4864,7 +4864,7 @@
             {
               "cipd_package": "chromium/testing/weblayer-x86",
               "location": "weblayer_instrumentation_test_M90",
-              "revision": "version:90.0.4430.43"
+              "revision": "version:90.0.4430.45"
             },
             {
               "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
@@ -5076,11 +5076,11 @@
             "--bucket",
             "chromium-result-details",
             "--test-name",
-            "weblayer_instrumentation_test_versions_apk_Client Library Skew Tests For 89.0.4389.110"
+            "weblayer_instrumentation_test_versions_apk_Client Library Skew Tests For 89.0.4389.112"
           ],
           "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
         },
-        "name": "weblayer_instrumentation_test_versions_apk_Client Library Skew Tests For 89.0.4389.110",
+        "name": "weblayer_instrumentation_test_versions_apk_Client Library Skew Tests For 89.0.4389.112",
         "resultdb": {
           "enable": true
         },
@@ -5090,7 +5090,7 @@
             {
               "cipd_package": "chromium/testing/weblayer-x86",
               "location": "weblayer_instrumentation_test_M89",
-              "revision": "version:89.0.4389.110"
+              "revision": "version:89.0.4389.112"
             },
             {
               "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
@@ -5155,11 +5155,11 @@
             "--bucket",
             "chromium-result-details",
             "--test-name",
-            "weblayer_instrumentation_test_versions_apk_Client Library Skew Tests For 90.0.4430.43"
+            "weblayer_instrumentation_test_versions_apk_Client Library Skew Tests For 90.0.4430.45"
           ],
           "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
         },
-        "name": "weblayer_instrumentation_test_versions_apk_Client Library Skew Tests For 90.0.4430.43",
+        "name": "weblayer_instrumentation_test_versions_apk_Client Library Skew Tests For 90.0.4430.45",
         "resultdb": {
           "enable": true
         },
@@ -5169,7 +5169,7 @@
             {
               "cipd_package": "chromium/testing/weblayer-x86",
               "location": "weblayer_instrumentation_test_M90",
-              "revision": "version:90.0.4430.43"
+              "revision": "version:90.0.4430.45"
             },
             {
               "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
@@ -5313,11 +5313,11 @@
             "--bucket",
             "chromium-result-details",
             "--test-name",
-            "weblayer_instrumentation_test_versions_apk_Implementation Library Skew Tests For 89.0.4389.110"
+            "weblayer_instrumentation_test_versions_apk_Implementation Library Skew Tests For 89.0.4389.112"
           ],
           "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
         },
-        "name": "weblayer_instrumentation_test_versions_apk_Implementation Library Skew Tests For 89.0.4389.110",
+        "name": "weblayer_instrumentation_test_versions_apk_Implementation Library Skew Tests For 89.0.4389.112",
         "resultdb": {
           "enable": true
         },
@@ -5327,7 +5327,7 @@
             {
               "cipd_package": "chromium/testing/weblayer-x86",
               "location": "weblayer_instrumentation_test_M89",
-              "revision": "version:89.0.4389.110"
+              "revision": "version:89.0.4389.112"
             },
             {
               "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
@@ -5392,11 +5392,11 @@
             "--bucket",
             "chromium-result-details",
             "--test-name",
-            "weblayer_instrumentation_test_versions_apk_Implementation Library Skew Tests For 90.0.4430.43"
+            "weblayer_instrumentation_test_versions_apk_Implementation Library Skew Tests For 90.0.4430.45"
           ],
           "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
         },
-        "name": "weblayer_instrumentation_test_versions_apk_Implementation Library Skew Tests For 90.0.4430.43",
+        "name": "weblayer_instrumentation_test_versions_apk_Implementation Library Skew Tests For 90.0.4430.45",
         "resultdb": {
           "enable": true
         },
@@ -5406,7 +5406,7 @@
             {
               "cipd_package": "chromium/testing/weblayer-x86",
               "location": "weblayer_instrumentation_test_M90",
-              "revision": "version:90.0.4430.43"
+              "revision": "version:90.0.4430.45"
             },
             {
               "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
diff --git a/testing/buildbot/chromium.android.json b/testing/buildbot/chromium.android.json
index 977dc89..104d0532 100644
--- a/testing/buildbot/chromium.android.json
+++ b/testing/buildbot/chromium.android.json
@@ -46456,11 +46456,11 @@
             "--bucket",
             "chromium-result-details",
             "--test-name",
-            "weblayer_instrumentation_test_versions_apk_Client Library Skew Tests For 89.0.4389.110"
+            "weblayer_instrumentation_test_versions_apk_Client Library Skew Tests For 89.0.4389.112"
           ],
           "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
         },
-        "name": "weblayer_instrumentation_test_versions_apk_Client Library Skew Tests For 89.0.4389.110",
+        "name": "weblayer_instrumentation_test_versions_apk_Client Library Skew Tests For 89.0.4389.112",
         "resultdb": {
           "enable": true
         },
@@ -46470,7 +46470,7 @@
             {
               "cipd_package": "chromium/testing/weblayer-x86",
               "location": "weblayer_instrumentation_test_M89",
-              "revision": "version:89.0.4389.110"
+              "revision": "version:89.0.4389.112"
             },
             {
               "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
@@ -46535,11 +46535,11 @@
             "--bucket",
             "chromium-result-details",
             "--test-name",
-            "weblayer_instrumentation_test_versions_apk_Client Library Skew Tests For 90.0.4430.43"
+            "weblayer_instrumentation_test_versions_apk_Client Library Skew Tests For 90.0.4430.45"
           ],
           "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
         },
-        "name": "weblayer_instrumentation_test_versions_apk_Client Library Skew Tests For 90.0.4430.43",
+        "name": "weblayer_instrumentation_test_versions_apk_Client Library Skew Tests For 90.0.4430.45",
         "resultdb": {
           "enable": true
         },
@@ -46549,7 +46549,7 @@
             {
               "cipd_package": "chromium/testing/weblayer-x86",
               "location": "weblayer_instrumentation_test_M90",
-              "revision": "version:90.0.4430.43"
+              "revision": "version:90.0.4430.45"
             },
             {
               "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
@@ -46693,11 +46693,11 @@
             "--bucket",
             "chromium-result-details",
             "--test-name",
-            "weblayer_instrumentation_test_versions_apk_Implementation Library Skew Tests For 89.0.4389.110"
+            "weblayer_instrumentation_test_versions_apk_Implementation Library Skew Tests For 89.0.4389.112"
           ],
           "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
         },
-        "name": "weblayer_instrumentation_test_versions_apk_Implementation Library Skew Tests For 89.0.4389.110",
+        "name": "weblayer_instrumentation_test_versions_apk_Implementation Library Skew Tests For 89.0.4389.112",
         "resultdb": {
           "enable": true
         },
@@ -46707,7 +46707,7 @@
             {
               "cipd_package": "chromium/testing/weblayer-x86",
               "location": "weblayer_instrumentation_test_M89",
-              "revision": "version:89.0.4389.110"
+              "revision": "version:89.0.4389.112"
             },
             {
               "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
@@ -46772,11 +46772,11 @@
             "--bucket",
             "chromium-result-details",
             "--test-name",
-            "weblayer_instrumentation_test_versions_apk_Implementation Library Skew Tests For 90.0.4430.43"
+            "weblayer_instrumentation_test_versions_apk_Implementation Library Skew Tests For 90.0.4430.45"
           ],
           "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
         },
-        "name": "weblayer_instrumentation_test_versions_apk_Implementation Library Skew Tests For 90.0.4430.43",
+        "name": "weblayer_instrumentation_test_versions_apk_Implementation Library Skew Tests For 90.0.4430.45",
         "resultdb": {
           "enable": true
         },
@@ -46786,7 +46786,7 @@
             {
               "cipd_package": "chromium/testing/weblayer-x86",
               "location": "weblayer_instrumentation_test_M90",
-              "revision": "version:90.0.4430.43"
+              "revision": "version:90.0.4430.45"
             },
             {
               "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
@@ -46997,11 +46997,11 @@
             "--bucket",
             "chromium-result-details",
             "--test-name",
-            "weblayer_instrumentation_test_versions_apk_Client Library Skew Tests For 89.0.4389.110"
+            "weblayer_instrumentation_test_versions_apk_Client Library Skew Tests For 89.0.4389.112"
           ],
           "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
         },
-        "name": "weblayer_instrumentation_test_versions_apk_Client Library Skew Tests For 89.0.4389.110",
+        "name": "weblayer_instrumentation_test_versions_apk_Client Library Skew Tests For 89.0.4389.112",
         "resultdb": {
           "enable": true
         },
@@ -47011,7 +47011,7 @@
             {
               "cipd_package": "chromium/testing/weblayer-x86",
               "location": "weblayer_instrumentation_test_M89",
-              "revision": "version:89.0.4389.110"
+              "revision": "version:89.0.4389.112"
             },
             {
               "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
@@ -47076,11 +47076,11 @@
             "--bucket",
             "chromium-result-details",
             "--test-name",
-            "weblayer_instrumentation_test_versions_apk_Client Library Skew Tests For 90.0.4430.43"
+            "weblayer_instrumentation_test_versions_apk_Client Library Skew Tests For 90.0.4430.45"
           ],
           "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
         },
-        "name": "weblayer_instrumentation_test_versions_apk_Client Library Skew Tests For 90.0.4430.43",
+        "name": "weblayer_instrumentation_test_versions_apk_Client Library Skew Tests For 90.0.4430.45",
         "resultdb": {
           "enable": true
         },
@@ -47090,7 +47090,7 @@
             {
               "cipd_package": "chromium/testing/weblayer-x86",
               "location": "weblayer_instrumentation_test_M90",
-              "revision": "version:90.0.4430.43"
+              "revision": "version:90.0.4430.45"
             },
             {
               "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
@@ -47234,11 +47234,11 @@
             "--bucket",
             "chromium-result-details",
             "--test-name",
-            "weblayer_instrumentation_test_versions_apk_Implementation Library Skew Tests For 89.0.4389.110"
+            "weblayer_instrumentation_test_versions_apk_Implementation Library Skew Tests For 89.0.4389.112"
           ],
           "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
         },
-        "name": "weblayer_instrumentation_test_versions_apk_Implementation Library Skew Tests For 89.0.4389.110",
+        "name": "weblayer_instrumentation_test_versions_apk_Implementation Library Skew Tests For 89.0.4389.112",
         "resultdb": {
           "enable": true
         },
@@ -47248,7 +47248,7 @@
             {
               "cipd_package": "chromium/testing/weblayer-x86",
               "location": "weblayer_instrumentation_test_M89",
-              "revision": "version:89.0.4389.110"
+              "revision": "version:89.0.4389.112"
             },
             {
               "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
@@ -47313,11 +47313,11 @@
             "--bucket",
             "chromium-result-details",
             "--test-name",
-            "weblayer_instrumentation_test_versions_apk_Implementation Library Skew Tests For 90.0.4430.43"
+            "weblayer_instrumentation_test_versions_apk_Implementation Library Skew Tests For 90.0.4430.45"
           ],
           "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
         },
-        "name": "weblayer_instrumentation_test_versions_apk_Implementation Library Skew Tests For 90.0.4430.43",
+        "name": "weblayer_instrumentation_test_versions_apk_Implementation Library Skew Tests For 90.0.4430.45",
         "resultdb": {
           "enable": true
         },
@@ -47327,7 +47327,7 @@
             {
               "cipd_package": "chromium/testing/weblayer-x86",
               "location": "weblayer_instrumentation_test_M90",
-              "revision": "version:90.0.4430.43"
+              "revision": "version:90.0.4430.45"
             },
             {
               "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
diff --git a/testing/buildbot/variants.pyl b/testing/buildbot/variants.pyl
index aec2f9e..f4abfb2 100644
--- a/testing/buildbot/variants.pyl
+++ b/testing/buildbot/variants.pyl
@@ -311,13 +311,13 @@
       '../../weblayer/browser/android/javatests/skew/expectations.txt',
       '--impl-version=90',
     ],
-    'identifier': 'Implementation Library Skew Tests For 90.0.4430.43',
+    'identifier': 'Implementation Library Skew Tests For 90.0.4430.45',
     'swarming': {
       'cipd_packages': [
         {
           'cipd_package': 'chromium/testing/weblayer-x86',
           'location': 'weblayer_instrumentation_test_M90',
-          'revision': 'version:90.0.4430.43',
+          'revision': 'version:90.0.4430.45',
         }
       ],
     },
@@ -335,13 +335,13 @@
       '../../weblayer/browser/android/javatests/skew/expectations.txt',
       '--impl-version=89',
     ],
-    'identifier': 'Implementation Library Skew Tests For 89.0.4389.110',
+    'identifier': 'Implementation Library Skew Tests For 89.0.4389.112',
     'swarming': {
       'cipd_packages': [
         {
           'cipd_package': 'chromium/testing/weblayer-x86',
           'location': 'weblayer_instrumentation_test_M89',
-          'revision': 'version:89.0.4389.110',
+          'revision': 'version:89.0.4389.112',
         }
       ],
     },
@@ -383,13 +383,13 @@
       '../../weblayer/browser/android/javatests/skew/expectations.txt',
       '--impl-version=90',
     ],
-    'identifier': 'Implementation Library Skew Tests For 90.0.4430.43',
+    'identifier': 'Implementation Library Skew Tests For 90.0.4430.45',
     'swarming': {
       'cipd_packages': [
         {
           'cipd_package': 'chromium/testing/weblayer-x86',
           'location': 'weblayer_instrumentation_test_M90',
-          'revision': 'version:90.0.4430.43',
+          'revision': 'version:90.0.4430.45',
         }
       ],
     },
@@ -407,13 +407,13 @@
       '../../weblayer/browser/android/javatests/skew/expectations.txt',
       '--impl-version=89',
     ],
-    'identifier': 'Implementation Library Skew Tests For 89.0.4389.110',
+    'identifier': 'Implementation Library Skew Tests For 89.0.4389.112',
     'swarming': {
       'cipd_packages': [
         {
           'cipd_package': 'chromium/testing/weblayer-x86',
           'location': 'weblayer_instrumentation_test_M89',
-          'revision': 'version:89.0.4389.110',
+          'revision': 'version:89.0.4389.112',
         }
       ],
     },
@@ -455,13 +455,13 @@
       '../../weblayer/browser/android/javatests/skew/expectations.txt',
       '--client-version=90',
     ],
-    'identifier': 'Client Library Skew Tests For 90.0.4430.43',
+    'identifier': 'Client Library Skew Tests For 90.0.4430.45',
     'swarming': {
       'cipd_packages': [
         {
           'cipd_package': 'chromium/testing/weblayer-x86',
           'location': 'weblayer_instrumentation_test_M90',
-          'revision': 'version:90.0.4430.43',
+          'revision': 'version:90.0.4430.45',
         }
       ],
     },
@@ -479,13 +479,13 @@
       '../../weblayer/browser/android/javatests/skew/expectations.txt',
       '--client-version=89',
     ],
-    'identifier': 'Client Library Skew Tests For 89.0.4389.110',
+    'identifier': 'Client Library Skew Tests For 89.0.4389.112',
     'swarming': {
       'cipd_packages': [
         {
           'cipd_package': 'chromium/testing/weblayer-x86',
           'location': 'weblayer_instrumentation_test_M89',
-          'revision': 'version:89.0.4389.110',
+          'revision': 'version:89.0.4389.112',
         }
       ],
     },
diff --git a/third_party/blink/renderer/bindings/scripts/generate_event_interfaces.py b/third_party/blink/renderer/bindings/scripts/generate_event_interfaces.py
index 0eae3af..1d5482b 100755
--- a/third_party/blink/renderer/bindings/scripts/generate_event_interfaces.py
+++ b/third_party/blink/renderer/bindings/scripts/generate_event_interfaces.py
@@ -52,7 +52,7 @@
     'ImplementedAs',
     'RuntimeEnabled',
 )
-module_path = os.path.dirname(os.path.realpath(__file__))
+module_path = os.path.dirname(os.path.abspath(__file__))
 REPO_ROOT_DIR = os.path.normpath(
     os.path.join(module_path, os.pardir, os.pardir, os.pardir, os.pardir,
                  os.pardir))
diff --git a/third_party/blink/renderer/core/accessibility/ax_object_cache.h b/third_party/blink/renderer/core/accessibility/ax_object_cache.h
index 71184946..c2a5af9c 100644
--- a/third_party/blink/renderer/core/accessibility/ax_object_cache.h
+++ b/third_party/blink/renderer/core/accessibility/ax_object_cache.h
@@ -83,9 +83,8 @@
   virtual void LocationChanged(const LayoutObject*) = 0;
   virtual void ImageLoaded(const LayoutObject*) = 0;
 
-  // Remove AXObject backed by passed-in object, if there is one.
   virtual void Remove(AccessibleNode*) = 0;
-  virtual bool Remove(LayoutObject*) = 0;  // Return true if AXObject removed.
+  virtual void Remove(LayoutObject*) = 0;
   virtual void Remove(Node*) = 0;
   virtual void Remove(AbstractInlineTextBox*) = 0;
 
diff --git a/third_party/blink/renderer/core/css/properties/css_parsing_utils.cc b/third_party/blink/renderer/core/css/properties/css_parsing_utils.cc
index c8d470c..204f9bb 100644
--- a/third_party/blink/renderer/core/css/properties/css_parsing_utils.cc
+++ b/third_party/blink/renderer/core/css/properties/css_parsing_utils.cc
@@ -375,6 +375,22 @@
   return true;
 }
 
+bool ConsumeNumbersOrPercents(CSSParserTokenRange& args,
+                              const CSSParserContext& context,
+                              CSSFunctionValue*& transform_value,
+                              unsigned number_of_arguments) {
+  do {
+    CSSValue* parsed_value =
+        ConsumeNumberOrPercent(args, context, kValueRangeAll);
+    if (!parsed_value)
+      return false;
+    transform_value->Append(*parsed_value);
+    if (--number_of_arguments && !ConsumeCommaIncludingWhitespace(args))
+      return false;
+  } while (number_of_arguments);
+  return true;
+}
+
 bool ConsumePerspective(CSSParserTokenRange& args,
                         const CSSParserContext& context,
                         CSSFunctionValue*& transform_value,
@@ -841,20 +857,24 @@
   return nullptr;
 }
 
-CSSPrimitiveValue* ConsumeAlphaValue(CSSParserTokenRange& range,
-                                     const CSSParserContext& context) {
-  if (CSSPrimitiveValue* value =
-          ConsumeNumber(range, context, kValueRangeAll)) {
+CSSPrimitiveValue* ConsumeNumberOrPercent(CSSParserTokenRange& range,
+                                          const CSSParserContext& context,
+                                          ValueRange value_range) {
+  if (CSSPrimitiveValue* value = ConsumeNumber(range, context, value_range)) {
     return value;
   }
-  if (CSSPrimitiveValue* value =
-          ConsumePercent(range, context, kValueRangeAll)) {
+  if (CSSPrimitiveValue* value = ConsumePercent(range, context, value_range)) {
     return CSSNumericLiteralValue::Create(value->GetDoubleValue() / 100.0,
                                           CSSPrimitiveValue::UnitType::kNumber);
   }
   return nullptr;
 }
 
+CSSPrimitiveValue* ConsumeAlphaValue(CSSParserTokenRange& range,
+                                     const CSSParserContext& context) {
+  return ConsumeNumberOrPercent(range, context, kValueRangeAll);
+}
+
 bool CanConsumeCalcValue(CalculationCategory category,
                          CSSParserMode css_parser_mode) {
   return category == kCalcLength || category == kCalcPercent ||
@@ -4706,13 +4726,13 @@
     case CSSValueID::kScaleY:
     case CSSValueID::kScaleZ:
     case CSSValueID::kScale:
-      parsed_value = ConsumeNumber(args, context, kValueRangeAll);
+      parsed_value = ConsumeNumberOrPercent(args, context, kValueRangeAll);
       if (!parsed_value)
         return nullptr;
       if (function_id == CSSValueID::kScale &&
           ConsumeCommaIncludingWhitespace(args)) {
         transform_value->Append(*parsed_value);
-        parsed_value = ConsumeNumber(args, context, kValueRangeAll);
+        parsed_value = ConsumeNumberOrPercent(args, context, kValueRangeAll);
         if (!parsed_value)
           return nullptr;
       }
@@ -4748,7 +4768,7 @@
       }
       break;
     case CSSValueID::kScale3d:
-      if (!ConsumeNumbers(args, context, transform_value, 3))
+      if (!ConsumeNumbersOrPercents(args, context, transform_value, 3))
         return nullptr;
       break;
     case CSSValueID::kRotate3d:
diff --git a/third_party/blink/renderer/core/css/properties/css_parsing_utils.h b/third_party/blink/renderer/core/css/properties/css_parsing_utils.h
index ac372d9..88ca49b 100644
--- a/third_party/blink/renderer/core/css/properties/css_parsing_utils.h
+++ b/third_party/blink/renderer/core/css/properties/css_parsing_utils.h
@@ -88,6 +88,12 @@
 CSSPrimitiveValue* ConsumePercent(CSSParserTokenRange&,
                                   const CSSParserContext&,
                                   ValueRange);
+
+// Any percentages are converted to numbers.
+CSSPrimitiveValue* ConsumeNumberOrPercent(CSSParserTokenRange&,
+                                          const CSSParserContext&,
+                                          ValueRange);
+
 CSSPrimitiveValue* ConsumeAlphaValue(CSSParserTokenRange&,
                                      const CSSParserContext&);
 CSSPrimitiveValue* ConsumeLengthOrPercent(
diff --git a/third_party/blink/renderer/core/css/properties/longhands/longhands_custom.cc b/third_party/blink/renderer/core/css/properties/longhands/longhands_custom.cc
index 29f2e52..99aafbc 100644
--- a/third_party/blink/renderer/core/css/properties/longhands/longhands_custom.cc
+++ b/third_party/blink/renderer/core/css/properties/longhands/longhands_custom.cc
@@ -5635,24 +5635,23 @@
   if (id == CSSValueID::kNone)
     return css_parsing_utils::ConsumeIdent(range);
 
-  CSSValue* x_scale =
-      css_parsing_utils::ConsumeNumber(range, context, kValueRangeAll);
+  CSSPrimitiveValue* x_scale =
+      css_parsing_utils::ConsumeNumberOrPercent(range, context, kValueRangeAll);
   if (!x_scale)
     return nullptr;
 
   CSSValueList* list = CSSValueList::CreateSpaceSeparated();
   list->Append(*x_scale);
 
-  CSSValue* y_scale =
-      css_parsing_utils::ConsumeNumber(range, context, kValueRangeAll);
+  CSSPrimitiveValue* y_scale =
+      css_parsing_utils::ConsumeNumberOrPercent(range, context, kValueRangeAll);
   if (y_scale) {
-    CSSValue* z_scale =
-        css_parsing_utils::ConsumeNumber(range, context, kValueRangeAll);
+    CSSPrimitiveValue* z_scale = css_parsing_utils::ConsumeNumberOrPercent(
+        range, context, kValueRangeAll);
     if (z_scale) {
       list->Append(*y_scale);
       list->Append(*z_scale);
-    } else if (To<CSSPrimitiveValue>(x_scale)->GetDoubleValue() !=
-               To<CSSPrimitiveValue>(y_scale)->GetDoubleValue()) {
+    } else if (x_scale->GetDoubleValue() != y_scale->GetDoubleValue()) {
       list->Append(*y_scale);
     }
   }
diff --git a/third_party/blink/renderer/core/dom/document.cc b/third_party/blink/renderer/core/dom/document.cc
index 40c41ea..8e45782 100644
--- a/third_party/blink/renderer/core/dom/document.cc
+++ b/third_party/blink/renderer/core/dom/document.cc
@@ -2558,6 +2558,7 @@
 }
 
 void Document::ApplyScrollRestorationLogic() {
+  DCHECK(View());
   // This function in not re-entrant. However, the places that invoke this are
   // re-entrant. Specifically, UpdateStyleAndLayout() calls this, which in turn
   // can do a find-in-page for the scroll-to-text feature, which can cause
@@ -2703,7 +2704,8 @@
   if (Lifecycle().GetState() < DocumentLifecycle::kLayoutClean)
     Lifecycle().AdvanceTo(DocumentLifecycle::kLayoutClean);
 
-  ApplyScrollRestorationLogic();
+  if (frame_view)
+    ApplyScrollRestorationLogic();
 
   if (LocalFrameView* frame_view_anchored = View())
     frame_view_anchored->PerformScrollAnchoringAdjustments();
diff --git a/third_party/blink/renderer/core/layout/ng/ng_block_layout_algorithm.cc b/third_party/blink/renderer/core/layout/ng/ng_block_layout_algorithm.cc
index ffab0038..034e508 100644
--- a/third_party/blink/renderer/core/layout/ng/ng_block_layout_algorithm.cc
+++ b/third_party/blink/renderer/core/layout/ng/ng_block_layout_algorithm.cc
@@ -186,16 +186,6 @@
           child_bfc_offset.block_offset - parent_bfc_offset.block_offset};
 }
 
-inline bool IsEarlyBreakpoint(const NGEarlyBreak& breakpoint,
-                              const NGBoxFragmentBuilder& builder,
-                              NGLayoutInputNode child) {
-  if (breakpoint.Type() == NGEarlyBreak::kLine)
-    return child.IsInline() && breakpoint.LineNumber() == builder.LineCount();
-  if (breakpoint.IsBreakBefore())
-    return breakpoint.BlockNode() == child;
-  return false;
-}
-
 }  // namespace
 
 NGBlockLayoutAlgorithm::NGBlockLayoutAlgorithm(
@@ -640,7 +630,7 @@
       // so now and finish layout.
       if (UNLIKELY(
               early_break_ &&
-              IsEarlyBreakpoint(*early_break_, container_builder_, child))) {
+              IsEarlyBreakTarget(*early_break_, container_builder_, child))) {
         if (!ResolveBfcBlockOffset(&previous_inflow_position)) {
           // However, the predetermined breakpoint may be exactly where the BFC
           // block-offset gets resolved. If that hasn't yet happened, we need to
diff --git a/third_party/blink/renderer/core/layout/ng/ng_column_layout_algorithm.cc b/third_party/blink/renderer/core/layout/ng/ng_column_layout_algorithm.cc
index bbea4d03..b310b6a 100644
--- a/third_party/blink/renderer/core/layout/ng/ng_column_layout_algorithm.cc
+++ b/third_party/blink/renderer/core/layout/ng/ng_column_layout_algorithm.cc
@@ -417,7 +417,8 @@
 
     // If this is the child we had previously determined to break before, do so
     // now and finish layout.
-    if (early_break_ && IsEarlyBreakTarget(spanner_node, *early_break_))
+    if (early_break_ &&
+        IsEarlyBreakTarget(*early_break_, container_builder_, spanner_node))
       break;
 
     NGBreakStatus break_status =
diff --git a/third_party/blink/renderer/core/layout/ng/ng_fragmentation_utils.cc b/third_party/blink/renderer/core/layout/ng/ng_fragmentation_utils.cc
index 1dede67..22588c3 100644
--- a/third_party/blink/renderer/core/layout/ng/ng_fragmentation_utils.cc
+++ b/third_party/blink/renderer/core/layout/ng/ng_fragmentation_utils.cc
@@ -764,14 +764,11 @@
   return early_break.BreakInside();
 }
 
-bool IsEarlyBreakTarget(const NGBlockNode& child,
-                        const NGEarlyBreak& early_break) {
-  // This utility function only support breaks before block nodes. Algorithms
-  // that need other types than that need to detect the break target on their
-  // own. Block container layout for instance also needs to consider lines
-  // (because of orphans and widows) and cannot use this function.
-  DCHECK_EQ(early_break.Type(), NGEarlyBreak::kBlock);
-
+bool IsEarlyBreakTarget(const NGEarlyBreak& early_break,
+                        const NGBoxFragmentBuilder& builder,
+                        const NGLayoutInputNode& child) {
+  if (early_break.Type() == NGEarlyBreak::kLine)
+    return child.IsInline() && early_break.LineNumber() == builder.LineCount();
   return early_break.IsBreakBefore() && early_break.BlockNode() == child;
 }
 
diff --git a/third_party/blink/renderer/core/layout/ng/ng_fragmentation_utils.h b/third_party/blink/renderer/core/layout/ng/ng_fragmentation_utils.h
index 26812f76..15843da 100644
--- a/third_party/blink/renderer/core/layout/ng/ng_fragmentation_utils.h
+++ b/third_party/blink/renderer/core/layout/ng/ng_fragmentation_utils.h
@@ -287,7 +287,9 @@
 
 // Return true if this is the child that we had previously determined to break
 // before.
-bool IsEarlyBreakTarget(const NGBlockNode& child, const NGEarlyBreak&);
+bool IsEarlyBreakTarget(const NGEarlyBreak&,
+                        const NGBoxFragmentBuilder&,
+                        const NGLayoutInputNode& child);
 
 // Calculate the constraint space for columns of a multi-column layout.
 NGConstraintSpace CreateConstraintSpaceForColumns(
diff --git a/third_party/blink/renderer/modules/accessibility/ax_object.cc b/third_party/blink/renderer/modules/accessibility/ax_object.cc
index 4846c94..9c5b2f92 100644
--- a/third_party/blink/renderer/modules/accessibility/ax_object.cc
+++ b/third_party/blink/renderer/modules/accessibility/ax_object.cc
@@ -776,11 +776,6 @@
 }
 
 void AXObject::Detach() {
-  // Not strictly necessary, but prevents LastKnown*() methods from returning
-  // the wrong values.
-  cached_is_ignored_ = true;
-  cached_is_ignored_but_included_in_tree_ = false;
-
 #if DCHECK_IS_ON()
   // Only mock objects can end up being detached twice, because their owner
   // may have needed to detach them when they were detached, but couldn't
@@ -2584,12 +2579,10 @@
 }
 
 bool AXObject::LastKnownIsIgnoredValue() const {
-  DCHECK(cached_is_ignored_ || !IsDetached());
   return cached_is_ignored_;
 }
 
 bool AXObject::LastKnownIsIgnoredButIncludedInTreeValue() const {
-  DCHECK(!cached_is_ignored_but_included_in_tree_ || !IsDetached());
   return cached_is_ignored_but_included_in_tree_;
 }
 
@@ -5515,10 +5508,6 @@
       string_builder = string_builder + " isInert";
     if (NeedsToUpdateChildren())
       string_builder = string_builder + " needsToUpdateChildren";
-    if (children_.size()) {
-      string_builder =
-          string_builder + " numChildren=" + String::Number(children_.size());
-    }
     if (!GetLayoutObject())
       string_builder = string_builder + " missingLayout";
 
diff --git a/third_party/blink/renderer/modules/accessibility/ax_object_cache_impl.cc b/third_party/blink/renderer/modules/accessibility/ax_object_cache_impl.cc
index 90f80b1..b04fde2 100644
--- a/third_party/blink/renderer/modules/accessibility/ax_object_cache_impl.cc
+++ b/third_party/blink/renderer/modules/accessibility/ax_object_cache_impl.cc
@@ -213,9 +213,7 @@
 }
 
 bool IsTextRelevantForAccessibility(const LayoutText& layout_text) {
-  if (!layout_text.Parent())
-    return false;
-
+  DCHECK(layout_text.Parent());
   Node* node = layout_text.GetNode();
   DCHECK(node);  // Anonymous text is processed earlier, doesn't reach here.
 
@@ -1115,7 +1113,7 @@
   if (!obj)
     return;
 
-  AXObject* parent = obj->CachedParentObject();
+  ChildrenChanged(obj->CachedParentObject());
 
   obj->Detach();
   RemoveAXID(obj);
@@ -1126,8 +1124,6 @@
   if (!objects_.Take(ax_id))
     return;
 
-  ChildrenChanged(parent);
-
   DCHECK_GE(objects_.size(), ids_in_use_.size());
 }
 
@@ -1136,23 +1132,18 @@
     return;
 
   AXID ax_id = accessible_node_mapping_.at(accessible_node);
-  accessible_node_mapping_.erase(accessible_node);
-
   Remove(ax_id);
+  accessible_node_mapping_.erase(accessible_node);
 }
 
-bool AXObjectCacheImpl::Remove(LayoutObject* layout_object) {
+void AXObjectCacheImpl::Remove(LayoutObject* layout_object) {
   if (!layout_object)
-    return false;
+    return;
 
   AXID ax_id = layout_object_mapping_.at(layout_object);
-  if (!ax_id)
-    return false;
 
-  layout_object_mapping_.erase(layout_object);
   Remove(ax_id);
-
-  return true;
+  layout_object_mapping_.erase(layout_object);
 }
 
 void AXObjectCacheImpl::Remove(Node* node) {
@@ -1161,10 +1152,11 @@
 
   // This is all safe even if we didn't have a mapping.
   AXID ax_id = node_object_mapping_.at(node);
+  Remove(ax_id);
   node_object_mapping_.erase(node);
 
-  if (!Remove(node->GetLayoutObject()))
-    Remove(ax_id);
+  if (node->GetLayoutObject())
+    Remove(node->GetLayoutObject());
 }
 
 void AXObjectCacheImpl::Remove(AbstractInlineTextBox* inline_text_box) {
@@ -1172,9 +1164,8 @@
     return;
 
   AXID ax_id = inline_text_box_object_mapping_.at(inline_text_box);
-  inline_text_box_object_mapping_.erase(inline_text_box);
-
   Remove(ax_id);
+  inline_text_box_object_mapping_.erase(inline_text_box);
 }
 
 AXID AXObjectCacheImpl::GenerateAXID() const {
@@ -1629,11 +1620,9 @@
 }
 
 void AXObjectCacheImpl::ChildrenChanged(AXObject* obj) {
-  if (!obj || obj->IsDetached())
+  if (!obj)
     return;
 
-  obj->SetNeedsToUpdateChildren();
-
   Node* node = obj->GetNode();
   if (node && !nodes_with_pending_children_changed_.insert(node).is_new_entry)
     return;
diff --git a/third_party/blink/renderer/modules/accessibility/ax_object_cache_impl.h b/third_party/blink/renderer/modules/accessibility/ax_object_cache_impl.h
index 2e16d9e..2362367 100644
--- a/third_party/blink/renderer/modules/accessibility/ax_object_cache_impl.h
+++ b/third_party/blink/renderer/modules/accessibility/ax_object_cache_impl.h
@@ -106,7 +106,7 @@
   void ImageLoaded(const LayoutObject*) override;
 
   void Remove(AccessibleNode*) override;
-  bool Remove(LayoutObject*) override;  // Return true of AXObject removed.
+  void Remove(LayoutObject*) override;
   void Remove(Node*) override;
   void Remove(AbstractInlineTextBox*) override;
   void Remove(AXObject*);  // Calls more specific Remove methods as necessary.
diff --git a/third_party/blink/renderer/modules/installation/installation_service_impl.cc b/third_party/blink/renderer/modules/installation/installation_service_impl.cc
index 5e4edd1f..faf76aaf 100644
--- a/third_party/blink/renderer/modules/installation/installation_service_impl.cc
+++ b/third_party/blink/renderer/modules/installation/installation_service_impl.cc
@@ -7,35 +7,56 @@
 #include <memory>
 #include <utility>
 
-#include "mojo/public/cpp/bindings/self_owned_receiver.h"
 #include "third_party/blink/renderer/core/dom/events/event.h"
 #include "third_party/blink/renderer/core/frame/local_dom_window.h"
 #include "third_party/blink/renderer/core/frame/local_frame.h"
 
 namespace blink {
 
-InstallationServiceImpl::InstallationServiceImpl(LocalFrame& frame)
-    : frame_(frame) {}
+// static
+const char InstallationServiceImpl::kSupplementName[] =
+    "InstallationServiceImpl";
 
 // static
-void InstallationServiceImpl::Create(
+InstallationServiceImpl* InstallationServiceImpl::From(LocalDOMWindow& window) {
+  return Supplement<LocalDOMWindow>::From<InstallationServiceImpl>(window);
+}
+
+// static
+void InstallationServiceImpl::BindReceiver(
     LocalFrame* frame,
     mojo::PendingReceiver<mojom::blink::InstallationService> receiver) {
+  DCHECK(frame && frame->DomWindow());
+  auto* service = InstallationServiceImpl::From(*frame->DomWindow());
+  if (!service) {
+    service = MakeGarbageCollected<InstallationServiceImpl>(
+        base::PassKey<InstallationServiceImpl>(), *frame);
+    Supplement<LocalDOMWindow>::ProvideTo(*frame->DomWindow(), service);
+  }
+  service->Bind(std::move(receiver));
+}
+
+InstallationServiceImpl::InstallationServiceImpl(
+    base::PassKey<InstallationServiceImpl>,
+    LocalFrame& frame)
+    : Supplement<LocalDOMWindow>(*frame.DomWindow()),
+      receivers_(this, frame.DomWindow()) {}
+
+void InstallationServiceImpl::Bind(
+    mojo::PendingReceiver<mojom::blink::InstallationService> receiver) {
   // See https://bit.ly/2S0zRAS for task types.
-  mojo::MakeSelfOwnedReceiver(std::make_unique<InstallationServiceImpl>(*frame),
-                              std::move(receiver),
-                              frame->GetTaskRunner(TaskType::kMiscPlatformAPI));
+  receivers_.Add(std::move(receiver), GetSupplementable()->GetTaskRunner(
+                                          TaskType::kMiscPlatformAPI));
+}
+
+void InstallationServiceImpl::Trace(Visitor* visitor) const {
+  visitor->Trace(receivers_);
+  Supplement<LocalDOMWindow>::Trace(visitor);
 }
 
 void InstallationServiceImpl::OnInstall() {
-  if (!frame_)
-    return;
-
-  LocalDOMWindow* dom_window = frame_->DomWindow();
-  if (!dom_window)
-    return;
-
-  dom_window->DispatchEvent(*Event::Create(event_type_names::kAppinstalled));
+  GetSupplementable()->DispatchEvent(
+      *Event::Create(event_type_names::kAppinstalled));
 }
 
 }  // namespace blink
diff --git a/third_party/blink/renderer/modules/installation/installation_service_impl.h b/third_party/blink/renderer/modules/installation/installation_service_impl.h
index 5e9b482..be26e69 100644
--- a/third_party/blink/renderer/modules/installation/installation_service_impl.h
+++ b/third_party/blink/renderer/modules/installation/installation_service_impl.h
@@ -5,27 +5,48 @@
 #ifndef THIRD_PARTY_BLINK_RENDERER_MODULES_INSTALLATION_INSTALLATION_SERVICE_IMPL_H_
 #define THIRD_PARTY_BLINK_RENDERER_MODULES_INSTALLATION_INSTALLATION_SERVICE_IMPL_H_
 
+#include "base/types/pass_key.h"
+#include "mojo/public/cpp/bindings/pending_receiver.h"
 #include "third_party/blink/public/mojom/installation/installation.mojom-blink.h"
 #include "third_party/blink/renderer/modules/modules_export.h"
-#include "third_party/blink/renderer/platform/heap/persistent.h"
+#include "third_party/blink/renderer/platform/heap/heap.h"
+#include "third_party/blink/renderer/platform/mojo/heap_mojo_receiver_set.h"
+#include "third_party/blink/renderer/platform/mojo/heap_mojo_wrapper_mode.h"
+#include "third_party/blink/renderer/platform/supplementable.h"
 
 namespace blink {
 
+class LocalDOMWindow;
 class LocalFrame;
 
 class MODULES_EXPORT InstallationServiceImpl final
-    : public mojom::blink::InstallationService {
+    : public GarbageCollected<InstallationServiceImpl>,
+      public mojom::blink::InstallationService,
+      public Supplement<LocalDOMWindow> {
  public:
-  explicit InstallationServiceImpl(LocalFrame&);
-
-  static void Create(
+  static const char kSupplementName[];
+  static InstallationServiceImpl* From(LocalDOMWindow&);
+  static void BindReceiver(
       LocalFrame*,
       mojo::PendingReceiver<mojom::blink::InstallationService> receiver);
 
+  explicit InstallationServiceImpl(base::PassKey<InstallationServiceImpl>,
+                                   LocalFrame&);
+
+  // Not copyable or movable
+  InstallationServiceImpl(const InstallationServiceImpl&) = delete;
+  InstallationServiceImpl& operator=(const InstallationServiceImpl&) = delete;
+
+  void Bind(mojo::PendingReceiver<mojom::blink::InstallationService> receiver);
+
+  void Trace(Visitor* visitor) const override;
+
   void OnInstall() override;
 
  private:
-  WeakPersistent<LocalFrame> frame_;
+  HeapMojoReceiverSet<mojom::blink::InstallationService,
+                      InstallationServiceImpl>
+      receivers_;
 };
 
 }  // namespace blink
diff --git a/third_party/blink/renderer/modules/modules_initializer.cc b/third_party/blink/renderer/modules/modules_initializer.cc
index eef11d7..f2c5639 100644
--- a/third_party/blink/renderer/modules/modules_initializer.cc
+++ b/third_party/blink/renderer/modules/modules_initializer.cc
@@ -169,7 +169,7 @@
       &FileHandlingExpiryImpl::Create, WrapWeakPersistent(&frame)));
 
   frame.GetInterfaceRegistry()->AddInterface(WTF::BindRepeating(
-      &InstallationServiceImpl::Create, WrapWeakPersistent(&frame)));
+      &InstallationServiceImpl::BindReceiver, WrapWeakPersistent(&frame)));
   // TODO(dominickn): This interface should be document-scoped rather than
   // frame-scoped, as the resulting banner event is dispatched to
   // frame()->document().
diff --git a/third_party/blink/renderer/modules/nfc/ndef_reader.cc b/third_party/blink/renderer/modules/nfc/ndef_reader.cc
index b13006e5..29fa6ff 100644
--- a/third_party/blink/renderer/modules/nfc/ndef_reader.cc
+++ b/third_party/blink/renderer/modules/nfc/ndef_reader.cc
@@ -189,10 +189,13 @@
 }
 
 void NDEFReader::OnReadingError(const String& message) {
-  DispatchEvent(*Event::Create(event_type_names::kReadingerror));
   GetExecutionContext()->AddConsoleMessage(MakeGarbageCollected<ConsoleMessage>(
       mojom::blink::ConsoleMessageSource::kJavaScript,
       mojom::blink::ConsoleMessageLevel::kInfo, message));
+
+  // Dispatch the event as the final step in this method as it may cause script
+  // to run that destroys the execution context.
+  DispatchEvent(*Event::Create(event_type_names::kReadingerror));
 }
 
 void NDEFReader::ContextDestroyed() {
diff --git a/third_party/blink/web_tests/external/wpt/css/css-transforms/parsing/scale-parsing-valid-expected.txt b/third_party/blink/web_tests/external/wpt/css/css-transforms/parsing/scale-parsing-valid-expected.txt
index 26caacd..393c8d1 100644
--- a/third_party/blink/web_tests/external/wpt/css/css-transforms/parsing/scale-parsing-valid-expected.txt
+++ b/third_party/blink/web_tests/external/wpt/css/css-transforms/parsing/scale-parsing-valid-expected.txt
@@ -1,24 +1,25 @@
 This is a testharness.js-based test.
 PASS e.style['scale'] = "none" should set the property value
 PASS e.style['scale'] = "1" should set the property value
-FAIL e.style['scale'] = "1%" should set the property value assert_not_equals: property should be set got disallowed value ""
+PASS e.style['scale'] = "1%" should set the property value
 PASS e.style['scale'] = "100" should set the property value
-FAIL e.style['scale'] = "100%" should set the property value assert_not_equals: property should be set got disallowed value ""
+PASS e.style['scale'] = "100%" should set the property value
 PASS e.style['scale'] = "100 100" should set the property value
-FAIL e.style['scale'] = "100% 100%" should set the property value assert_not_equals: property should be set got disallowed value ""
+PASS e.style['scale'] = "100% 100%" should set the property value
 FAIL e.style['scale'] = "100 100 1" should set the property value assert_equals: serialization should be canonical expected "100" but got "100 100 1"
-FAIL e.style['scale'] = "100% 100% 1" should set the property value assert_not_equals: property should be set got disallowed value ""
+FAIL e.style['scale'] = "100% 100% 1" should set the property value assert_equals: serialization should be canonical expected "1" but got "1 1 1"
 PASS e.style['scale'] = "-100" should set the property value
-FAIL e.style['scale'] = "-100%" should set the property value assert_not_equals: property should be set got disallowed value ""
+PASS e.style['scale'] = "-100%" should set the property value
 PASS e.style['scale'] = "-100 -100" should set the property value
-FAIL e.style['scale'] = "-100% -100%" should set the property value assert_not_equals: property should be set got disallowed value ""
+PASS e.style['scale'] = "-100% -100%" should set the property value
 FAIL e.style['scale'] = "-100 -100 1" should set the property value assert_equals: serialization should be canonical expected "-100" but got "-100 -100 1"
-FAIL e.style['scale'] = "-100% -100% 1" should set the property value assert_not_equals: property should be set got disallowed value ""
+FAIL e.style['scale'] = "-100% -100% 1" should set the property value assert_equals: serialization should be canonical expected "-1" but got "-1 -1 1"
 PASS e.style['scale'] = "100 200" should set the property value
-FAIL e.style['scale'] = "100% 200%" should set the property value assert_not_equals: property should be set got disallowed value ""
+PASS e.style['scale'] = "100% 200%" should set the property value
 FAIL e.style['scale'] = "100 200 1" should set the property value assert_equals: serialization should be canonical expected "100 200" but got "100 200 1"
-FAIL e.style['scale'] = "100% 200% 1" should set the property value assert_not_equals: property should be set got disallowed value ""
+FAIL e.style['scale'] = "100% 200% 1" should set the property value assert_equals: serialization should be canonical expected "1 2" but got "1 2 1"
 PASS e.style['scale'] = "100 200 300" should set the property value
 PASS e.style['scale'] = "100 100 2" should set the property value
+PASS e.style['scale'] = "100% 200% 300%" should set the property value
 Harness: the test ran to completion.
 
diff --git a/third_party/blink/web_tests/external/wpt/css/css-transforms/parsing/scale-parsing-valid.html b/third_party/blink/web_tests/external/wpt/css/css-transforms/parsing/scale-parsing-valid.html
index 46bb91c..ecdc5c0 100644
--- a/third_party/blink/web_tests/external/wpt/css/css-transforms/parsing/scale-parsing-valid.html
+++ b/third_party/blink/web_tests/external/wpt/css/css-transforms/parsing/scale-parsing-valid.html
@@ -36,7 +36,7 @@
 test_valid_value("scale", "100% 200% 1", "1 2");
 test_valid_value("scale", "100 200 300");
 test_valid_value("scale", "100 100 2", "100 100 2");
-
+test_valid_value("scale", "100% 200% 300%", "1 2 3");
 </script>
 </body>
 </html>
diff --git a/third_party/blink/web_tests/external/wpt/css/css-transforms/parsing/transform-valid-expected.txt b/third_party/blink/web_tests/external/wpt/css/css-transforms/parsing/transform-valid-expected.txt
deleted file mode 100644
index 994797e..0000000
--- a/third_party/blink/web_tests/external/wpt/css/css-transforms/parsing/transform-valid-expected.txt
+++ /dev/null
@@ -1,38 +0,0 @@
-This is a testharness.js-based test.
-PASS e.style['transform'] = "none" should set the property value
-PASS e.style['transform'] = "matrix(1, 0, 0, 1, 0, 0)" should set the property value
-PASS e.style['transform'] = "matrix(1, 2, 3, 4, 5, 6)" should set the property value
-PASS e.style['transform'] = "matrix(-0.1, -0.2, -0.3, -0.4, -0.5, -0.6)" should set the property value
-PASS e.style['transform'] = "translate(1px)" should set the property value
-PASS e.style['transform'] = "translate(2%, -3%)" should set the property value
-PASS e.style['transform'] = "translateX(-4px)" should set the property value
-PASS e.style['transform'] = "translateY(5%)" should set the property value
-PASS e.style['transform'] = "scale(2)" should set the property value
-PASS e.style['transform'] = "scale(3, 4)" should set the property value
-PASS e.style['transform'] = "scale(-2)" should set the property value
-PASS e.style['transform'] = "scale(-5, -6)" should set the property value
-FAIL e.style['transform'] = "scale(250%)" should set the property value assert_not_equals: property should be set got disallowed value ""
-FAIL e.style['transform'] = "scale(325%, 475%)" should set the property value assert_not_equals: property should be set got disallowed value ""
-FAIL e.style['transform'] = "scale(-250%)" should set the property value assert_not_equals: property should be set got disallowed value ""
-FAIL e.style['transform'] = "scale(-500%, -620%)" should set the property value assert_not_equals: property should be set got disallowed value ""
-PASS e.style['transform'] = "scaleX(7)" should set the property value
-FAIL e.style['transform'] = "scaleX(720%)" should set the property value assert_not_equals: property should be set got disallowed value ""
-PASS e.style['transform'] = "scaleY(-8)" should set the property value
-FAIL e.style['transform'] = "scaleY(-85%)" should set the property value assert_not_equals: property should be set got disallowed value ""
-PASS e.style['transform'] = "scale3d(0.5, 2.5, 3)" should set the property value
-FAIL e.style['transform'] = "scale3d(50%, 250%, 300%)" should set the property value assert_not_equals: property should be set got disallowed value ""
-PASS e.style['transform'] = "scale3d(-0.5, 2.5, -3)" should set the property value
-FAIL e.style['transform'] = "scale3d(-50%, 250%, -300%)" should set the property value assert_not_equals: property should be set got disallowed value ""
-PASS e.style['transform'] = "rotate(0)" should set the property value
-PASS e.style['transform'] = "rotate(90deg)" should set the property value
-PASS e.style['transform'] = "skew(0)" should set the property value
-PASS e.style['transform'] = "skew(90deg)" should set the property value
-PASS e.style['transform'] = "skew(0, -90deg)" should set the property value
-PASS e.style['transform'] = "skew(90deg, 0)" should set the property value
-PASS e.style['transform'] = "skewX(0)" should set the property value
-PASS e.style['transform'] = "skewX(90deg)" should set the property value
-PASS e.style['transform'] = "skewY(0)" should set the property value
-PASS e.style['transform'] = "skewY(-90deg)" should set the property value
-PASS e.style['transform'] = "translate(1px, 2%) scale(3, 4) rotate(-90deg)" should set the property value
-Harness: the test ran to completion.
-
diff --git a/third_party/blink/web_tests/external/wpt/css/css-transforms/parsing/transform-valid.html b/third_party/blink/web_tests/external/wpt/css/css-transforms/parsing/transform-valid.html
index ce32e2b..ba4f6e2 100644
--- a/third_party/blink/web_tests/external/wpt/css/css-transforms/parsing/transform-valid.html
+++ b/third_party/blink/web_tests/external/wpt/css/css-transforms/parsing/transform-valid.html
@@ -32,6 +32,7 @@
 
 test_valid_value("transform", "scale(250%)", "scale(2.5)");
 test_valid_value("transform", "scale(325%, 475%)", "scale(3.25, 4.75)");
+test_valid_value("transform", "scale(1, 200%)", "scale(1, 2)");
 
 test_valid_value("transform", "scale(-250%)", "scale(-2.5)");
 test_valid_value("transform", "scale(-500%, -620%)", "scale(-5, -6.2)");
@@ -42,11 +43,15 @@
 test_valid_value("transform", "scaleY(-8)");
 test_valid_value("transform", "scaleY(-85%)", "scaleY(-0.85)");
 
+test_valid_value("transform", "scaleZ(4)");
+test_valid_value("transform", "scaleZ(25%)", "scaleZ(0.25)");
+
 test_valid_value("transform", "scale3d(0.5, 2.5, 3)");
 test_valid_value("transform", "scale3d(50%, 250%, 300%)", "scale3d(0.5, 2.5, 3)");
 
 test_valid_value("transform", "scale3d(-0.5, 2.5, -3)");
 test_valid_value("transform", "scale3d(-50%, 250%, -300%)", "scale3d(-0.5, 2.5, -3)");
+test_valid_value("transform", "scale3d(1, 200%, 3)", "scale3d(1, 2, 3)");
 
 test_valid_value("transform", "rotate(0)", "rotate(0deg)");
 test_valid_value("transform", "rotate(90deg)");
diff --git a/third_party/blink/web_tests/transforms/scale-parsing.html b/third_party/blink/web_tests/transforms/scale-parsing.html
index ba32d295..64ee7f6 100644
--- a/third_party/blink/web_tests/transforms/scale-parsing.html
+++ b/third_party/blink/web_tests/transforms/scale-parsing.html
@@ -15,13 +15,14 @@
 expect('5 5').parsesAs('5').isComputedTo('5');
 expect('1 -2.3').parsesAs('1 -2.3').isComputedTo('1 -2.3');
 expect('1 -2.3 4').parsesAs('1 -2.3 4').isComputedTo('1 -2.3 4');
+expect('30%').parsesAs('0.3').isComputedTo('0.3');
 
 expect('calc(1 * 2)').parsesAs('2').isComputedTo('2');
 expect('calc(1 * 2) calc(2 / 4)').parsesAs('2 0.5').isComputedTo('2 0.5');
 expect('calc(1 * 2) calc(2 / 4) calc(0.5)').parsesAs('2 0.5 0.5').isComputedTo('2 0.5 0.5');
+expect('calc(1 * 2) calc(2 / 4) calc(0.5%)').parsesAs('2 0.5 0.005').isComputedTo('2 0.5 0.005');
 
 expect('2px').isInvalid();
-expect('30%').isInvalid();
 expect('1 1 1 1').isInvalid();
 expect('1 2 3deg').isInvalid();
 expect('1 + 1').isInvalid();
@@ -29,5 +30,4 @@
 expect('1, 2,').isInvalid();
 expect('calc(1 * 2 + 4px) calc(2 / 4) calc(0.5)').isInvalid();
 expect('calc(1 * 2) calc(2 / 4 * 4deg) calc(0.5)').isInvalid();
-expect('calc(1 * 2) calc(2 / 4) calc(0.5%)').isInvalid();
 </script>
diff --git a/third_party/blink/web_tests/transforms/transform-parsing-expected.txt b/third_party/blink/web_tests/transforms/transform-parsing-expected.txt
index 89ecb159..79d1684 100644
--- a/third_party/blink/web_tests/transforms/transform-parsing-expected.txt
+++ b/third_party/blink/web_tests/transforms/transform-parsing-expected.txt
@@ -40,23 +40,26 @@
 PASS "transform: scale(1);" should parse as "scale(1)"
 PASS "transform: scale(2, 3);" should parse as "scale(2, 3)"
 PASS "transform: scale(2, 3);" should be computed to "matrix(2, 0, 0, 3, 0, 0)"
+PASS "transform: scale(20%, 50%);" should parse as "scale(0.2, 0.5)"
+PASS "transform: scale(20%, 50%);" should be computed to "matrix(0.2, 0, 0, 0.5, 0, 0)"
 PASS "transform: scale();" should be invalid
 PASS "transform: scale(1,);" should be invalid
 PASS "transform: scale(1, 2, 3);" should be invalid
 PASS "transform: scale(1px, 2px);" should be invalid
-PASS "transform: scale(20%, 50%);" should be invalid
 PASS "transform: scaleX(2);" should parse as "scaleX(2)"
 PASS "transform: scaleX(2);" should be computed to "matrix(2, 0, 0, 1, 0, 0)"
+PASS "transform: scaleX(50%);" should parse as "scaleX(0.5)"
+PASS "transform: scaleX(50%);" should be computed to "matrix(0.5, 0, 0, 1, 0, 0)"
 PASS "transform: scaleX();" should be invalid
 PASS "transform: scaleX(1, 2);" should be invalid
 PASS "transform: scaleX(1px);" should be invalid
-PASS "transform: scaleX(50%);" should be invalid
 PASS "transform: scaleY(2);" should parse as "scaleY(2)"
 PASS "transform: scaleY(2);" should be computed to "matrix(1, 0, 0, 2, 0, 0)"
+PASS "transform: scaleY(50%);" should parse as "scaleY(0.5)"
+PASS "transform: scaleY(50%);" should be computed to "matrix(1, 0, 0, 0.5, 0, 0)"
 PASS "transform: scaleY();" should be invalid
 PASS "transform: scaleY(1, 2);" should be invalid
 PASS "transform: scaleY(1px);" should be invalid
-PASS "transform: scaleY(50%);" should be invalid
 PASS "transform: rotate(0);" should parse as "rotate(0deg)"
 PASS "transform: rotate(1deg);" should parse as "rotate(1deg)"
 PASS "transform: rotate(2grad);" should parse as "rotate(2grad)"
@@ -113,16 +116,18 @@
 PASS "transform: translateZ(1px, 2px);" should be invalid
 PASS "transform: scale3d(2, 3, 4);" should parse as "scale3d(2, 3, 4)"
 PASS "transform: scale3d(2, 3, 4);" should be computed to "matrix3d(2, 0, 0, 0, 0, 3, 0, 0, 0, 0, 4, 0, 0, 0, 0, 1)"
+PASS "transform: scale3d(20%, 50%, 60%);" should parse as "scale3d(0.2, 0.5, 0.6)"
+PASS "transform: scale3d(20%, 50%, 60%);" should be computed to "matrix3d(0.2, 0, 0, 0, 0, 0.5, 0, 0, 0, 0, 0.6, 0, 0, 0, 0, 1)"
 PASS "transform: scale3d();" should be invalid
 PASS "transform: scale3d(1, 2, 3, 4);" should be invalid
 PASS "transform: scale3d(1px, 2px, 3px);" should be invalid
-PASS "transform: scale3d(20%, 50%, 60%);" should be invalid
 PASS "transform: scaleZ(2);" should parse as "scaleZ(2)"
 PASS "transform: scaleZ(2);" should be computed to "matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 2, 0, 0, 0, 0, 1)"
+PASS "transform: scaleZ(50%);" should parse as "scaleZ(0.5)"
+PASS "transform: scaleZ(50%);" should be computed to "matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0.5, 0, 0, 0, 0, 1)"
 PASS "transform: scaleZ();" should be invalid
 PASS "transform: scaleZ(1, 2);" should be invalid
 PASS "transform: scaleZ(1px);" should be invalid
-PASS "transform: scaleZ(50%);" should be invalid
 PASS "transform: rotate3d(0, 0, 0, 0);" should parse as "rotate3d(0, 0, 0, 0deg)"
 PASS "transform: rotate3d(1, 2, 3, 4deg);" should parse as "rotate3d(1, 2, 3, 4deg)"
 PASS "transform: rotate3d();" should be invalid
diff --git a/third_party/blink/web_tests/transforms/transform-parsing.html b/third_party/blink/web_tests/transforms/transform-parsing.html
index 225d042..d7ddf43 100644
--- a/third_party/blink/web_tests/transforms/transform-parsing.html
+++ b/third_party/blink/web_tests/transforms/transform-parsing.html
@@ -54,25 +54,25 @@
 // scale()
 expect('scale(1)').parsesAs('scale(1)');
 expect('scale(2, 3)').parsesAs('scale(2, 3)').isComputedTo('matrix(2, 0, 0, 3, 0, 0)');
+expect('scale(20%, 50%)').parsesAs('scale(0.2, 0.5)').isComputedTo('matrix(0.2, 0, 0, 0.5, 0, 0)');
 expect('scale()').isInvalid();
 expect('scale(1,)').isInvalid();
 expect('scale(1, 2, 3)').isInvalid();
 expect('scale(1px, 2px)').isInvalid();
-expect('scale(20%, 50%)').isInvalid();
 
 // scaleX()
 expect('scaleX(2)').parsesAs('scaleX(2)').isComputedTo('matrix(2, 0, 0, 1, 0, 0)');
+expect('scaleX(50%)').parsesAs('scaleX(0.5)').isComputedTo('matrix(0.5, 0, 0, 1, 0, 0)');
 expect('scaleX()').isInvalid();
 expect('scaleX(1, 2)').isInvalid();
 expect('scaleX(1px)').isInvalid();
-expect('scaleX(50%)').isInvalid();
 
 // scaleY()
 expect('scaleY(2)').parsesAs('scaleY(2)').isComputedTo('matrix(1, 0, 0, 2, 0, 0)');
+expect('scaleY(50%)').parsesAs('scaleY(0.5)').isComputedTo('matrix(1, 0, 0, 0.5, 0, 0)');
 expect('scaleY()').isInvalid();
 expect('scaleY(1, 2)').isInvalid();
 expect('scaleY(1px)').isInvalid();
-expect('scaleY(50%)').isInvalid();
 
 // rotate()
 expect('rotate(0)').parsesAs('rotate(0deg)');
@@ -146,18 +146,20 @@
 // scale3d()
 expect('scale3d(2, 3, 4)').parsesAs('scale3d(2, 3, 4)').
     isComputedTo('matrix3d(2, 0, 0, 0, 0, 3, 0, 0, 0, 0, 4, 0, 0, 0, 0, 1)');
+expect('scale3d(20%, 50%, 60%)').parsesAs('scale3d(0.2, 0.5, 0.6)').
+    isComputedTo('matrix3d(0.2, 0, 0, 0, 0, 0.5, 0, 0, 0, 0, 0.6, 0, 0, 0, 0, 1)');
 expect('scale3d()').isInvalid();
 expect('scale3d(1, 2, 3, 4)').isInvalid();
 expect('scale3d(1px, 2px, 3px)').isInvalid();
-expect('scale3d(20%, 50%, 60%)').isInvalid();
 
 // scaleZ()
 expect('scaleZ(2)').parsesAs('scaleZ(2)').
     isComputedTo('matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 2, 0, 0, 0, 0, 1)');
+expect('scaleZ(50%)').parsesAs('scaleZ(0.5)').
+    isComputedTo('matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0.5, 0, 0, 0, 0, 1)');
 expect('scaleZ()').isInvalid();
 expect('scaleZ(1, 2)').isInvalid();
 expect('scaleZ(1px)').isInvalid();
-expect('scaleZ(50%)').isInvalid();
 
 // rotate3d()
 expect('rotate3d(0, 0, 0, 0)').parsesAs('rotate3d(0, 0, 0, 0deg)');
diff --git a/tools/mb/mb.py b/tools/mb/mb.py
index 9275cb0..ed94368 100755
--- a/tools/mb/mb.py
+++ b/tools/mb/mb.py
@@ -461,13 +461,16 @@
     return 0
 
   def RtsSelect(self):
-    exe = self.PathJoin(self.chromium_src_dir, 'testing', 'rts', 'rts-chromium')
+    model_dir = self.PathJoin(
+        self.chromium_src_dir, 'testing', 'rts', self._CipdPlatform())
+
+    exe = self.PathJoin(model_dir, 'rts-chromium')
     if self.platform == 'win32':
       exe += '.exe'
 
     args = [
        exe, 'select',
-      '-model-dir', self.PathJoin(self.chromium_src_dir, 'testing', 'rts'), \
+      '-model-dir', model_dir, \
       '-out', self.PathJoin(self.ToAbsPath(self.args.path), self.rts_out_dir),
       '-checkout', self.chromium_src_dir,
     ]
@@ -1517,7 +1520,6 @@
       cmd += ['--dotfile=' + self.args.dotfile]
     return cmd + [path] + list(args)
 
-
   def GNArgs(self, vals, expand_imports=False):
     gn_args = vals['gn_args']
 
@@ -1893,7 +1895,6 @@
       raise MBErr('Error %s writing to the output path "%s"' %
                  (e, path))
 
-
   def PrintCmd(self, cmd):
     if self.platform == 'win32':
       shell_quoter = QuoteForCmd
@@ -1951,6 +1952,17 @@
       out = err = ''
     return p.returncode, out, err
 
+  def _CipdPlatform(self):
+    """Returns current CIPD platform, e.g. linux-amd64.
+
+    Assumes AMD64.
+    """
+    if self.platform == 'win32':
+      return 'windows-amd64'
+    if self.platform == 'darwin':
+      return 'mac-amd64'
+    return 'linux-amd64'
+
   def ExpandUser(self, path):
     # This function largely exists so it can be overridden for testing.
     return os.path.expanduser(path)
diff --git a/tools/metrics/histograms/enums.xml b/tools/metrics/histograms/enums.xml
index c0614f3..69d8126 100644
--- a/tools/metrics/histograms/enums.xml
+++ b/tools/metrics/histograms/enums.xml
@@ -24019,6 +24019,7 @@
   <int value="838" label="SerialAllowUsbDevicesForUrls"/>
   <int value="839" label="ForcedLanguages"/>
   <int value="840" label="BrowserThemeColor"/>
+  <int value="841" label="CECPQ2Enabled"/>
 </enum>
 
 <enum name="EnterprisePolicyDeviceIdValidity">
@@ -44538,6 +44539,7 @@
   <int value="-1383145700"
       label="AutofillDoNotMigrateUnsupportedLocalCards:enabled"/>
   <int value="-1382671832" label="OmniboxUIExperimentVerticalMargin:enabled"/>
+  <int value="-1378071979" label="CroshSWA:enabled"/>
   <int value="-1377186702" label="DesktopIOSPromotion:disabled"/>
   <int value="-1376510363" label="ServiceWorkerScriptFullCodeCache:disabled"/>
   <int value="-1376006534" label="WinUseBrowserSpellChecker:disabled"/>
@@ -44657,6 +44659,7 @@
       label="AutofillEnableCardNicknameManagement:enabled"/>
   <int value="-1285021473" label="save-page-as-mhtml"/>
   <int value="-1284637134" label="pull-to-refresh"/>
+  <int value="-1283164264" label="CroshSWA:disabled"/>
   <int value="-1282992935"
       label="AutofillLocalCardMigrationShowFeedback:disabled"/>
   <int value="-1281465357" label="MacV2GPUSandbox:disabled"/>
diff --git a/tools/perf/core/bot_platforms.py b/tools/perf/core/bot_platforms.py
index 1c81adb..8cd3f33 100644
--- a/tools/perf/core/bot_platforms.py
+++ b/tools/perf/core/bot_platforms.py
@@ -469,8 +469,10 @@
     _GetBenchmarkConfig('rendering.desktop'),
     _GetBenchmarkConfig('system_health.common_desktop')
 ])
-_FUCHSIA_PERF_FYI_BENCHMARK_CONFIGS = PerfSuite(
-    [_GetBenchmarkConfig('system_health.memory_desktop')])
+_FUCHSIA_PERF_FYI_BENCHMARK_CONFIGS = PerfSuite([
+    _GetBenchmarkConfig('system_health.memory_desktop'),
+    _GetBenchmarkConfig('media.desktop')
+])
 _LINUX_PERF_CALIBRATION_BENCHMARK_CONFIGS = PerfSuite([
     _GetBenchmarkConfig('speedometer2'),
 ])
diff --git a/tools/perf/core/shard_maps/fuchsia-perf-fyi_map.json b/tools/perf/core/shard_maps/fuchsia-perf-fyi_map.json
index 58a485d..5e059399 100644
--- a/tools/perf/core/shard_maps/fuchsia-perf-fyi_map.json
+++ b/tools/perf/core/shard_maps/fuchsia-perf-fyi_map.json
@@ -1,8 +1,11 @@
 {
     "0": {
         "benchmarks": {
+            "media.desktop": {
+                "abridged": false
+            },
             "system_health.memory_desktop": {
-                "end": 6,
+                "end": 10,
                 "abridged": false
             }
         }
@@ -10,8 +13,8 @@
     "1": {
         "benchmarks": {
             "system_health.memory_desktop": {
-                "begin": 6,
-                "end": 18,
+                "begin": 10,
+                "end": 25,
                 "abridged": false
             }
         }
@@ -19,8 +22,8 @@
     "2": {
         "benchmarks": {
             "system_health.memory_desktop": {
-                "begin": 18,
-                "end": 30,
+                "begin": 25,
+                "end": 36,
                 "abridged": false
             }
         }
@@ -28,8 +31,8 @@
     "3": {
         "benchmarks": {
             "system_health.memory_desktop": {
-                "begin": 30,
-                "end": 43,
+                "begin": 36,
+                "end": 49,
                 "abridged": false
             }
         }
@@ -37,8 +40,8 @@
     "4": {
         "benchmarks": {
             "system_health.memory_desktop": {
-                "begin": 43,
-                "end": 55,
+                "begin": 49,
+                "end": 58,
                 "abridged": false
             }
         }
@@ -46,8 +49,8 @@
     "5": {
         "benchmarks": {
             "system_health.memory_desktop": {
-                "begin": 55,
-                "end": 68,
+                "begin": 58,
+                "end": 73,
                 "abridged": false
             }
         }
@@ -55,23 +58,23 @@
     "6": {
         "benchmarks": {
             "system_health.memory_desktop": {
-                "begin": 68,
+                "begin": 73,
                 "abridged": false
             }
         }
     },
     "extra_infos": {
-        "num_stories": 80,
-        "predicted_min_shard_time": 120,
-        "predicted_min_shard_index": 1,
-        "predicted_max_shard_time": 131.0,
-        "predicted_max_shard_index": 0,
-        "shard #0": 131.0,
-        "shard #1": 120,
-        "shard #2": 120,
-        "shard #3": 130,
-        "shard #4": 120,
-        "shard #5": 130,
-        "shard #6": 120
+        "num_stories": 105,
+        "predicted_min_shard_time": 790.0,
+        "predicted_min_shard_index": 0,
+        "predicted_max_shard_time": 930.0,
+        "predicted_max_shard_index": 1,
+        "shard #0": 790.0,
+        "shard #1": 930.0,
+        "shard #2": 930.0,
+        "shard #3": 921.0,
+        "shard #4": 873.0,
+        "shard #5": 891.0,
+        "shard #6": 891.0
     }
 }
\ No newline at end of file
diff --git a/tools/perf/core/shard_maps/timing_data/fuchsia-perf-fyi_timing.json b/tools/perf/core/shard_maps/timing_data/fuchsia-perf-fyi_timing.json
index e558a39..f64abb2 100644
--- a/tools/perf/core/shard_maps/timing_data/fuchsia-perf-fyi_timing.json
+++ b/tools/perf/core/shard_maps/timing_data/fuchsia-perf-fyi_timing.json
@@ -1,6 +1,322 @@
 [
     {
+        "duration": "2.0",
+        "name": "system_health.memory_desktop/browse:media:googleplaystore:2021"
+    },
+    {
+        "duration": "2.0",
+        "name": "system_health.memory_desktop/browse:media:imgur"
+    },
+    {
+        "duration": "1.0",
+        "name": "system_health.memory_desktop/browse:media:pinterest:2018"
+    },
+    {
+        "duration": "1.0",
+        "name": "system_health.memory_desktop/browse:media:tumblr:2018"
+    },
+    {
+        "duration": "105.0",
+        "name": "system_health.memory_desktop/browse:media:youtube:2019"
+    },
+    {
+        "duration": "1.0",
+        "name": "system_health.memory_desktop/browse:media:youtubetv:2019"
+    },
+    {
+        "duration": "1.0",
+        "name": "system_health.memory_desktop/browse:media:youtubetv_watch:2020"
+    },
+    {
+        "duration": "1.0",
+        "name": "system_health.memory_desktop/browse:news:cnn:2021"
+    },
+    {
+        "duration": "67.0",
+        "name": "system_health.memory_desktop/browse:news:flipboard:2020"
+    },
+    {
+        "duration": "75.0",
+        "name": "system_health.memory_desktop/browse:news:hackernews:2020"
+    },
+    {
+        "duration": "1.0",
+        "name": "system_health.memory_desktop/browse:news:nytimes:2020"
+    },
+    {
+        "duration": "1.0",
+        "name": "system_health.memory_desktop/browse:news:reddit:2020"
+    },
+    {
+        "duration": "67.0",
+        "name": "system_health.memory_desktop/browse:search:google:2020"
+    },
+    {
+        "duration": "1.0",
+        "name": "system_health.memory_desktop/browse:search:google_india:2021"
+    },
+    {
+        "duration": "1.0",
+        "name": "system_health.memory_desktop/browse:social:facebook_infinite_scroll:2018"
+    },
+    {
+        "duration": "86.0",
+        "name": "system_health.memory_desktop/browse:social:tumblr_infinite_scroll:2018"
+    },
+    {
+        "duration": "1.0",
+        "name": "system_health.memory_desktop/browse:social:twitter:2018"
+    },
+    {
+        "duration": "102.0",
+        "name": "system_health.memory_desktop/browse:social:twitter_infinite_scroll:2018"
+    },
+    {
+        "duration": "1.0",
+        "name": "system_health.memory_desktop/browse:tech:discourse_infinite_scroll:2018"
+    },
+    {
+        "duration": "2.0",
+        "name": "system_health.memory_desktop/browse:tools:autocad:2021"
+    },
+    {
+        "duration": "50.0",
+        "name": "system_health.memory_desktop/browse:tools:docs_scrolling"
+    },
+    {
+        "duration": "1.0",
+        "name": "system_health.memory_desktop/browse:tools:earth:2020"
+    },
+    {
+        "duration": "1.0",
+        "name": "system_health.memory_desktop/browse:tools:gmail-compose:2020"
+    },
+    {
+        "duration": "1.0",
+        "name": "system_health.memory_desktop/browse:tools:gmail-labelclick:2020"
+    },
+    {
+        "duration": "1.0",
+        "name": "system_health.memory_desktop/browse:tools:gmail-openconversation:2020"
+    },
+    {
+        "duration": "1.0",
+        "name": "system_health.memory_desktop/browse:tools:gmail-search:2020"
+    },
+    {
+        "duration": "2.0",
+        "name": "system_health.memory_desktop/browse:tools:maps:2019"
+    },
+    {
+        "duration": "2.0",
+        "name": "system_health.memory_desktop/browse:tools:sheets:2019"
+    },
+    {
+        "duration": "53.0",
+        "name": "system_health.memory_desktop/browse_accessibility:media:youtube"
+    },
+    {
+        "duration": "2.0",
+        "name": "system_health.memory_desktop/browse_accessibility:tech:codesearch:2018"
+    },
+    {
         "duration": "27.0",
         "name": "system_health.memory_desktop/load:chrome:blank"
+    },
+    {
+        "duration": "30.0",
+        "name": "system_health.memory_desktop/load:games:alphabetty:2018"
+    },
+    {
+        "duration": "26.0",
+        "name": "system_health.memory_desktop/load:games:bubbles:2020"
+    },
+    {
+        "duration": "30.0",
+        "name": "system_health.memory_desktop/load:games:lazors"
+    },
+    {
+        "duration": "35.0",
+        "name": "system_health.memory_desktop/load:games:miniclip:2018"
+    },
+    {
+        "duration": "2.0",
+        "name": "system_health.memory_desktop/load:games:spychase:2018"
+    },
+    {
+        "duration": "36.0",
+        "name": "system_health.memory_desktop/load:media:9gag"
+    },
+    {
+        "duration": "36.0",
+        "name": "system_health.memory_desktop/load:media:dailymotion:2019"
+    },
+    {
+        "duration": "2.0",
+        "name": "system_health.memory_desktop/load:media:facebook_feed:desktop:2020"
+    },
+    {
+        "duration": "27.0",
+        "name": "system_health.memory_desktop/load:media:facebook_photos:2018"
+    },
+    {
+        "duration": "2.0",
+        "name": "system_health.memory_desktop/load:media:facebook_photos:desktop:2020"
+    },
+    {
+        "duration": "35.0",
+        "name": "system_health.memory_desktop/load:media:flickr:2018"
+    },
+    {
+        "duration": "29.0",
+        "name": "system_health.memory_desktop/load:media:google_images:2018"
+    },
+    {
+        "duration": "31.0",
+        "name": "system_health.memory_desktop/load:media:imgur:2018"
+    },
+    {
+        "duration": "36.0",
+        "name": "system_health.memory_desktop/load:media:soundcloud:2018"
+    },
+    {
+        "duration": "41.0",
+        "name": "system_health.memory_desktop/load:media:youtube:2018"
+    },
+    {
+        "duration": "25.0",
+        "name": "system_health.memory_desktop/load:media:youtubelivingroom:2020"
+    },
+    {
+        "duration": "34.0",
+        "name": "system_health.memory_desktop/load:news:bbc:2018"
+    },
+    {
+        "duration": "2.0",
+        "name": "system_health.memory_desktop/load:news:cnn:2020"
+    },
+    {
+        "duration": "35.0",
+        "name": "system_health.memory_desktop/load:news:flipboard"
+    },
+    {
+        "duration": "25.0",
+        "name": "system_health.memory_desktop/load:news:hackernews:2018"
+    },
+    {
+        "duration": "52.0",
+        "name": "system_health.memory_desktop/load:news:nytimes:2018"
+    },
+    {
+        "duration": "33.0",
+        "name": "system_health.memory_desktop/load:news:qq:2018"
+    },
+    {
+        "duration": "39.0",
+        "name": "system_health.memory_desktop/load:news:reddit:2018"
+    },
+    {
+        "duration": "33.0",
+        "name": "system_health.memory_desktop/load:news:wikipedia:2018"
+    },
+    {
+        "duration": "33.0",
+        "name": "system_health.memory_desktop/load:search:amazon:2018"
+    },
+    {
+        "duration": "29.0",
+        "name": "system_health.memory_desktop/load:search:baidu:2018"
+    },
+    {
+        "duration": "32.0",
+        "name": "system_health.memory_desktop/load:search:ebay:2018"
+    },
+    {
+        "duration": "34.0",
+        "name": "system_health.memory_desktop/load:search:flipkart:2018"
+    },
+    {
+        "duration": "27.0",
+        "name": "system_health.memory_desktop/load:search:google:2018"
+    },
+    {
+        "duration": "37.0",
+        "name": "system_health.memory_desktop/load:search:taobao:2018"
+    },
+    {
+        "duration": "26.0",
+        "name": "system_health.memory_desktop/load:search:yahoo:2018"
+    },
+    {
+        "duration": "28.0",
+        "name": "system_health.memory_desktop/load:search:yandex:2018"
+    },
+    {
+        "duration": "28.0",
+        "name": "system_health.memory_desktop/load:social:instagram:2018"
+    },
+    {
+        "duration": "25.0",
+        "name": "system_health.memory_desktop/load:social:pinterest:2019"
+    },
+    {
+        "duration": "31.0",
+        "name": "system_health.memory_desktop/load:social:vk:2018"
+    },
+    {
+        "duration": "2.0",
+        "name": "system_health.memory_desktop/load:tools:chat:2020"
+    },
+    {
+        "duration": "58.0",
+        "name": "system_health.memory_desktop/load:tools:docs:2019"
+    },
+    {
+        "duration": "28.0",
+        "name": "system_health.memory_desktop/load:tools:drive:2019"
+    },
+    {
+        "duration": "2.0",
+        "name": "system_health.memory_desktop/load:tools:gmail:2019"
+    },
+    {
+        "duration": "32.0",
+        "name": "system_health.memory_desktop/load:tools:stackoverflow:2018"
+    },
+    {
+        "duration": "35.0",
+        "name": "system_health.memory_desktop/load:tools:weather:2019"
+    },
+    {
+        "duration": "31.0",
+        "name": "system_health.memory_desktop/load_accessibility:media:wikipedia:2018"
+    },
+    {
+        "duration": "61.0",
+        "name": "system_health.memory_desktop/load_accessibility:shopping:amazon:2018"
+    },
+    {
+        "duration": "2.0",
+        "name": "system_health.memory_desktop/long_running:tools:gmail-background"
+    },
+    {
+        "duration": "2.0",
+        "name": "system_health.memory_desktop/long_running:tools:gmail-foreground"
+    },
+    {
+        "duration": "2.0",
+        "name": "system_health.memory_desktop/multitab:misc:typical24"
+    },
+    {
+        "duration": "1.0",
+        "name": "system_health.memory_desktop/multitab:misc:typical24:2018"
+    },
+    {
+        "duration": "2.0",
+        "name": "system_health.memory_desktop/play:media:google_play_music"
+    },
+    {
+        "duration": "68.0",
+        "name": "system_health.memory_desktop/play:media:soundcloud:2018"
     }
 ]
\ No newline at end of file
diff --git a/tools/perf/expectations.config b/tools/perf/expectations.config
index 9f26571..84fb745 100644
--- a/tools/perf/expectations.config
+++ b/tools/perf/expectations.config
@@ -186,6 +186,10 @@
 # Benchmark: media.desktop
 crbug.com/1114681 [ win-laptop ] media.desktop/video.html?src=smpte_3840x2160_60fps_vp9.webm&seek [ Skip ]
 crbug.com/1095610 [ win-laptop ] media.desktop/video.html?src=garden2_10s.mp4&seek [ Skip ]
+crbug.com/1192303 [ fuchsia ] media.desktop/video.html?src=tulip2.vp9.webm&background [ Skip ]
+crbug.com/1192303 [ fuchsia ] media.desktop/mse.html?media=aac_audio.mp4,h264_video.mp4 [ Skip ]
+crbug.com/1192303 [ fuchsia ] media.desktop/mse.html?media=tulip2.vp9.webm [ Skip ]
+crbug.com/1192303 [ fuchsia ] media.desktop/mse.html?media=tulip0.av1.mp4 [ Skip ]
 
 # Benchmark: power.desktop
 crbug.com/1036360 [ win7 ] power.desktop/abcnews [ Skip ]
diff --git a/ui/gfx/animation/keyframe/animation_curve.cc b/ui/gfx/animation/keyframe/animation_curve.cc
index 645225d..17fcc23 100644
--- a/ui/gfx/animation/keyframe/animation_curve.cc
+++ b/ui/gfx/animation/keyframe/animation_curve.cc
@@ -44,5 +44,6 @@
 DEFINE_ANIMATION_CURVE(Float, FLOAT)
 DEFINE_ANIMATION_CURVE(Size, SIZE)
 DEFINE_ANIMATION_CURVE(Color, COLOR)
+DEFINE_ANIMATION_CURVE(Rect, RECT)
 
 }  // namespace gfx
diff --git a/ui/gfx/animation/keyframe/animation_curve.h b/ui/gfx/animation/keyframe/animation_curve.h
index 8d119bf..bffb4c7 100644
--- a/ui/gfx/animation/keyframe/animation_curve.h
+++ b/ui/gfx/animation/keyframe/animation_curve.h
@@ -10,6 +10,7 @@
 #include "base/time/time.h"
 #include "third_party/skia/include/core/SkColor.h"
 #include "ui/gfx/animation/keyframe/keyframe_animation_export.h"
+#include "ui/gfx/geometry/rect.h"
 #include "ui/gfx/geometry/size_f.h"
 #include "ui/gfx/transform.h"
 #include "ui/gfx/transform_operations.h"
@@ -31,13 +32,11 @@
     FLOAT,
     TRANSFORM,
     SIZE,
+    RECT,
 
     // cc:: curve types.
     FILTER,
     SCROLL_OFFSET,
-
-    // This must be last
-    LAST_CURVE_TYPE = SIZE,
   };
 
   virtual ~AnimationCurve() = default;
@@ -104,6 +103,10 @@
   DECLARE_ANIMATION_CURVE_BODY(gfx::TransformOperations, Transform)
 };
 
+class GFX_KEYFRAME_ANIMATION_EXPORT RectAnimationCurve : public AnimationCurve {
+  DECLARE_ANIMATION_CURVE_BODY(gfx::Rect, Rect)
+};
+
 }  // namespace gfx
 
 #endif  // UI_GFX_ANIMATION_KEYFRAME_ANIMATION_CURVE_H_
diff --git a/ui/gfx/animation/keyframe/keyframed_animation_curve.cc b/ui/gfx/animation/keyframe/keyframed_animation_curve.cc
index c08f646..90671be1 100644
--- a/ui/gfx/animation/keyframe/keyframed_animation_curve.cc
+++ b/ui/gfx/animation/keyframe/keyframed_animation_curve.cc
@@ -132,6 +132,32 @@
   return SizeKeyframe::Create(Time(), Value(), std::move(func));
 }
 
+std::unique_ptr<RectKeyframe> RectKeyframe::Create(
+    base::TimeDelta time,
+    const gfx::Rect& value,
+    std::unique_ptr<TimingFunction> timing_function) {
+  return base::WrapUnique(
+      new RectKeyframe(time, value, std::move(timing_function)));
+}
+
+RectKeyframe::RectKeyframe(base::TimeDelta time,
+                           const gfx::Rect& value,
+                           std::unique_ptr<TimingFunction> timing_function)
+    : Keyframe(time, std::move(timing_function)), value_(value) {}
+
+RectKeyframe::~RectKeyframe() = default;
+
+const gfx::Rect& RectKeyframe::Value() const {
+  return value_;
+}
+
+std::unique_ptr<RectKeyframe> RectKeyframe::Clone() const {
+  std::unique_ptr<TimingFunction> func;
+  if (timing_function())
+    func = timing_function()->Clone();
+  return RectKeyframe::Create(Time(), Value(), std::move(func));
+}
+
 std::unique_ptr<KeyframedColorAnimationCurve>
 KeyframedColorAnimationCurve::Create() {
   return base::WrapUnique(new KeyframedColorAnimationCurve);
@@ -373,4 +399,59 @@
                                        keyframes_[i + 1]->Value());
 }
 
+std::unique_ptr<KeyframedRectAnimationCurve>
+KeyframedRectAnimationCurve::Create() {
+  return base::WrapUnique(new KeyframedRectAnimationCurve);
+}
+
+KeyframedRectAnimationCurve::KeyframedRectAnimationCurve()
+    : scaled_duration_(1.0) {}
+
+KeyframedRectAnimationCurve::~KeyframedRectAnimationCurve() = default;
+
+void KeyframedRectAnimationCurve::AddKeyframe(
+    std::unique_ptr<RectKeyframe> keyframe) {
+  InsertKeyframe(std::move(keyframe), &keyframes_);
+}
+
+base::TimeDelta KeyframedRectAnimationCurve::Duration() const {
+  return (keyframes_.back()->Time() - keyframes_.front()->Time()) *
+         scaled_duration();
+}
+
+base::TimeDelta KeyframedRectAnimationCurve::TickInterval() const {
+  return ComputeTickInterval(timing_function_, scaled_duration(), keyframes_);
+}
+
+std::unique_ptr<AnimationCurve> KeyframedRectAnimationCurve::Clone() const {
+  std::unique_ptr<KeyframedRectAnimationCurve> to_return =
+      KeyframedRectAnimationCurve::Create();
+  for (const auto& keyframe : keyframes_)
+    to_return->AddKeyframe(keyframe->Clone());
+
+  if (timing_function_)
+    to_return->SetTimingFunction(timing_function_->Clone());
+
+  to_return->set_scaled_duration(scaled_duration());
+
+  return std::move(to_return);
+}
+
+gfx::Rect KeyframedRectAnimationCurve::GetValue(base::TimeDelta t) const {
+  if (t <= (keyframes_.front()->Time() * scaled_duration()))
+    return keyframes_.front()->Value();
+
+  if (t >= (keyframes_.back()->Time() * scaled_duration()))
+    return keyframes_.back()->Value();
+
+  t = TransformedAnimationTime(keyframes_, timing_function_, scaled_duration(),
+                               t);
+  size_t i = GetActiveKeyframe(keyframes_, scaled_duration(), t);
+  double progress =
+      TransformedKeyframeProgress(keyframes_, scaled_duration(), t, i);
+
+  return gfx::Tween::RectValueBetween(progress, keyframes_[i]->Value(),
+                                      keyframes_[i + 1]->Value());
+}
+
 }  // namespace gfx
diff --git a/ui/gfx/animation/keyframe/keyframed_animation_curve.h b/ui/gfx/animation/keyframe/keyframed_animation_curve.h
index 50bbb3b8..cf35b9e2 100644
--- a/ui/gfx/animation/keyframe/keyframed_animation_curve.h
+++ b/ui/gfx/animation/keyframe/keyframed_animation_curve.h
@@ -13,6 +13,7 @@
 #include "ui/gfx/animation/keyframe/animation_curve.h"
 #include "ui/gfx/animation/keyframe/keyframe_animation_export.h"
 #include "ui/gfx/animation/keyframe/timing_function.h"
+#include "ui/gfx/geometry/rect.h"
 #include "ui/gfx/geometry/size_f.h"
 #include "ui/gfx/transform_operations.h"
 
@@ -118,6 +119,26 @@
   gfx::SizeF value_;
 };
 
+class GFX_KEYFRAME_ANIMATION_EXPORT RectKeyframe : public Keyframe {
+ public:
+  static std::unique_ptr<RectKeyframe> Create(
+      base::TimeDelta time,
+      const gfx::Rect& value,
+      std::unique_ptr<TimingFunction> timing_function);
+  ~RectKeyframe() override;
+
+  const gfx::Rect& Value() const;
+
+  std::unique_ptr<RectKeyframe> Clone() const;
+
+ private:
+  RectKeyframe(base::TimeDelta time,
+               const gfx::Rect& value,
+               std::unique_ptr<TimingFunction> timing_function);
+
+  gfx::Rect value_;
+};
+
 class GFX_KEYFRAME_ANIMATION_EXPORT KeyframedColorAnimationCurve
     : public ColorAnimationCurve {
  public:
@@ -287,6 +308,45 @@
   double scaled_duration_;
 };
 
+class GFX_KEYFRAME_ANIMATION_EXPORT KeyframedRectAnimationCurve
+    : public RectAnimationCurve {
+ public:
+  // It is required that the keyframes be sorted by time.
+  static std::unique_ptr<KeyframedRectAnimationCurve> Create();
+
+  KeyframedRectAnimationCurve(const KeyframedRectAnimationCurve&) = delete;
+  ~KeyframedRectAnimationCurve() override;
+
+  KeyframedRectAnimationCurve& operator=(const KeyframedRectAnimationCurve&) =
+      delete;
+
+  void AddKeyframe(std::unique_ptr<RectKeyframe> keyframe);
+  void SetTimingFunction(std::unique_ptr<TimingFunction> timing_function) {
+    timing_function_ = std::move(timing_function);
+  }
+  double scaled_duration() const { return scaled_duration_; }
+  void set_scaled_duration(double scaled_duration) {
+    scaled_duration_ = scaled_duration;
+  }
+
+  // AnimationCurve implementation
+  base::TimeDelta Duration() const override;
+  std::unique_ptr<AnimationCurve> Clone() const override;
+  base::TimeDelta TickInterval() const override;
+
+  // RectAnimationCurve implementation
+  gfx::Rect GetValue(base::TimeDelta t) const override;
+
+ private:
+  KeyframedRectAnimationCurve();
+
+  // Always sorted in order of increasing time. No two keyframes have the
+  // same time.
+  std::vector<std::unique_ptr<RectKeyframe>> keyframes_;
+  std::unique_ptr<TimingFunction> timing_function_;
+  double scaled_duration_ = 0.;
+};
+
 }  // namespace gfx
 
 #endif  // UI_GFX_ANIMATION_KEYFRAME_KEYFRAMED_ANIMATION_CURVE_H_
diff --git a/ui/gfx/animation/keyframe/keyframed_animation_curve_unittest.cc b/ui/gfx/animation/keyframe/keyframed_animation_curve_unittest.cc
index c83350b..0242882 100644
--- a/ui/gfx/animation/keyframe/keyframed_animation_curve_unittest.cc
+++ b/ui/gfx/animation/keyframe/keyframed_animation_curve_unittest.cc
@@ -916,6 +916,98 @@
   EXPECT_SIZEF_EQ(size_b, curve->GetValue(base::TimeDelta::FromSecondsD(3.f)));
 }
 
+// Tests that a rect animation with one keyframe works as expected.
+TEST(KeyframedAnimationCurveTest, OneRectKeyFrame) {
+  gfx::Rect rect = gfx::Rect(1, 2, 101, 102);
+  std::unique_ptr<KeyframedRectAnimationCurve> curve(
+      KeyframedRectAnimationCurve::Create());
+  curve->AddKeyframe(RectKeyframe::Create(base::TimeDelta(), rect, nullptr));
+
+  EXPECT_EQ(rect, curve->GetValue(base::TimeDelta::FromSecondsD(-1.f)));
+  EXPECT_EQ(rect, curve->GetValue(base::TimeDelta::FromSecondsD(0.f)));
+  EXPECT_EQ(rect, curve->GetValue(base::TimeDelta::FromSecondsD(0.5f)));
+  EXPECT_EQ(rect, curve->GetValue(base::TimeDelta::FromSecondsD(1.f)));
+  EXPECT_EQ(rect, curve->GetValue(base::TimeDelta::FromSecondsD(2.f)));
+}
+
+// Tests that a rect animation with two keyframes works as expected.
+TEST(KeyframedAnimationCurveTest, TwoRectKeyFrame) {
+  gfx::Rect rect_a = gfx::Rect(1, 2, 100, 100);
+  gfx::Rect rect_b = gfx::Rect(11, 12, 100, 0);
+  gfx::Rect rect_midpoint = gfx::Tween::RectValueBetween(0.5, rect_a, rect_b);
+  std::unique_ptr<KeyframedRectAnimationCurve> curve(
+      KeyframedRectAnimationCurve::Create());
+  curve->AddKeyframe(RectKeyframe::Create(base::TimeDelta(), rect_a, nullptr));
+  curve->AddKeyframe(RectKeyframe::Create(base::TimeDelta::FromSecondsD(1.0),
+                                          rect_b, nullptr));
+
+  EXPECT_EQ(rect_a, curve->GetValue(base::TimeDelta::FromSecondsD(-1.f)));
+  EXPECT_EQ(rect_a, curve->GetValue(base::TimeDelta::FromSecondsD(0.f)));
+  EXPECT_EQ(rect_midpoint,
+            curve->GetValue(base::TimeDelta::FromSecondsD(0.5f)));
+  EXPECT_EQ(rect_b, curve->GetValue(base::TimeDelta::FromSecondsD(1.f)));
+  EXPECT_EQ(rect_b, curve->GetValue(base::TimeDelta::FromSecondsD(2.f)));
+}
+
+// Tests that a rect animation with three keyframes works as expected.
+TEST(KeyframedAnimationCurveTest, ThreeRectKeyFrame) {
+  gfx::Rect rect_a = gfx::Rect(1, 2, 100, 100);
+  gfx::Rect rect_b = gfx::Rect(11, 12, 100, 0);
+  gfx::Rect rect_c = gfx::Rect(101, 102, 200, 0);
+  gfx::Rect rect_midpoint1 = gfx::Tween::RectValueBetween(0.5, rect_a, rect_b);
+  gfx::Rect rect_midpoint2 = gfx::Tween::RectValueBetween(0.5, rect_b, rect_c);
+  std::unique_ptr<KeyframedRectAnimationCurve> curve(
+      KeyframedRectAnimationCurve::Create());
+  curve->AddKeyframe(RectKeyframe::Create(base::TimeDelta(), rect_a, nullptr));
+  curve->AddKeyframe(RectKeyframe::Create(base::TimeDelta::FromSecondsD(1.0),
+                                          rect_b, nullptr));
+  curve->AddKeyframe(RectKeyframe::Create(base::TimeDelta::FromSecondsD(2.0),
+                                          rect_c, nullptr));
+
+  EXPECT_EQ(rect_a, curve->GetValue(base::TimeDelta::FromSecondsD(-1.f)));
+  EXPECT_EQ(rect_a, curve->GetValue(base::TimeDelta::FromSecondsD(0.f)));
+  EXPECT_EQ(rect_midpoint1,
+            curve->GetValue(base::TimeDelta::FromSecondsD(0.5f)));
+  EXPECT_EQ(rect_b, curve->GetValue(base::TimeDelta::FromSecondsD(1.f)));
+  EXPECT_EQ(rect_midpoint2,
+            curve->GetValue(base::TimeDelta::FromSecondsD(1.5f)));
+  EXPECT_EQ(rect_c, curve->GetValue(base::TimeDelta::FromSecondsD(2.f)));
+  EXPECT_EQ(rect_c, curve->GetValue(base::TimeDelta::FromSecondsD(3.f)));
+}
+
+// Tests that a rect animation with multiple keys at a given time works sanely.
+TEST(KeyframedAnimationCurveTest, RepeatedRectKeyFrame) {
+  gfx::Rect rect_a = gfx::Rect(10, 20, 100, 64);
+  gfx::Rect rect_b = gfx::Rect(30, 40, 100, 192);
+
+  std::unique_ptr<KeyframedRectAnimationCurve> curve(
+      KeyframedRectAnimationCurve::Create());
+  curve->AddKeyframe(RectKeyframe::Create(base::TimeDelta(), rect_a, nullptr));
+  curve->AddKeyframe(RectKeyframe::Create(base::TimeDelta::FromSecondsD(1.0),
+                                          rect_a, nullptr));
+  curve->AddKeyframe(RectKeyframe::Create(base::TimeDelta::FromSecondsD(1.0),
+                                          rect_b, nullptr));
+  curve->AddKeyframe(RectKeyframe::Create(base::TimeDelta::FromSecondsD(2.0),
+                                          rect_b, nullptr));
+
+  EXPECT_EQ(rect_a, curve->GetValue(base::TimeDelta::FromSecondsD(-1.f)));
+  EXPECT_EQ(rect_a, curve->GetValue(base::TimeDelta::FromSecondsD(0.f)));
+  EXPECT_EQ(rect_a, curve->GetValue(base::TimeDelta::FromSecondsD(0.5f)));
+
+  gfx::Rect value = curve->GetValue(base::TimeDelta::FromSecondsD(1.0f));
+  EXPECT_EQ(100, value.width());
+  EXPECT_LE(64, value.height());
+  EXPECT_GE(192, value.height());
+  EXPECT_LE(10, value.x());
+  EXPECT_GE(30, value.x());
+  EXPECT_LE(20, value.y());
+  EXPECT_GE(40, value.y());
+
+  EXPECT_EQ(rect_b, curve->GetValue(base::TimeDelta::FromSecondsD(1.5f)));
+  EXPECT_EQ(rect_b, curve->GetValue(base::TimeDelta::FromSecondsD(2.f)));
+  EXPECT_EQ(rect_b, curve->GetValue(base::TimeDelta::FromSecondsD(3.f)));
+}
+
 // Tests that the computing of tick interval for STEPS TimingFunction works
 // correctly.
 TEST(KeyFrameAnimationCurveTest, TickIntervalForStepsTimingFunction) {
diff --git a/ui/gl/dc_layer_tree.cc b/ui/gl/dc_layer_tree.cc
index 0826042..8a8bd38 100644
--- a/ui/gl/dc_layer_tree.cc
+++ b/ui/gl/dc_layer_tree.cc
@@ -6,6 +6,8 @@
 
 #include "ui/gl/dc_layer_tree.h"
 
+#include <utility>
+
 #include "base/memory/ptr_util.h"
 #include "base/metrics/histogram_functions.h"
 #include "base/trace_event/trace_event.h"
@@ -186,7 +188,7 @@
     DirectCompositionChildSurfaceWin* root_surface) {
   TRACE_EVENT1("gpu", "DCLayerTree::CommitAndClearPendingOverlays",
                "num_pending_overlays", pending_overlays_.size());
-  DCHECK(!needs_rebuild_visual_tree_);
+  DCHECK(!needs_rebuild_visual_tree_ || ink_renderer_->HasBeenInitialized());
   bool needs_commit = false;
   // Check if root surface visual needs a commit first.
   if (!root_surface_visual_) {
@@ -252,7 +254,9 @@
 
   // Rebuild visual tree and commit if any visual changed.
   // Note: needs_rebuild_visual_tree_ might be set in this function and in
-  // SetNeedsRebuildVisualTree() during video_swap_chain->PresentToSwapChain()
+  // SetNeedsRebuildVisualTree() during video_swap_chain->PresentToSwapChain().
+  // Can also be set in DCLayerTree::SetDelegatedInkTrailStartPoint to add a
+  // delegated ink visual into the tree.
   if (needs_rebuild_visual_tree_) {
     TRACE_EVENT0(
         "gpu", "DCLayerTree::CommitAndClearPendingOverlays::ReBuildVisualTree");
@@ -336,4 +340,20 @@
                                   nullptr);
 }
 
+void DCLayerTree::SetDelegatedInkTrailStartPoint(
+    std::unique_ptr<gfx::DelegatedInkMetadata> metadata) {
+  DCHECK(SupportsDelegatedInk());
+
+  if (!ink_renderer_->HasBeenInitialized()) {
+    if (!InitializeInkRenderer())
+      return;
+    // This ensures that the delegated ink visual is added to the tree after
+    // the root visual is created, during
+    // DCLayerTree::CommitAndClearPendingOverlays
+    needs_rebuild_visual_tree_ = true;
+  }
+
+  ink_renderer_->SetDelegatedInkTrailStartPoint(std::move(metadata));
+}
+
 }  // namespace gl
diff --git a/ui/gl/dc_layer_tree.h b/ui/gl/dc_layer_tree.h
index 056d7c3d..0bcfc41 100644
--- a/ui/gl/dc_layer_tree.h
+++ b/ui/gl/dc_layer_tree.h
@@ -19,6 +19,10 @@
 #include "ui/gl/delegated_ink_point_renderer_gpu.h"
 #include "ui/gl/hdr_metadata_helper_win.h"
 
+namespace gfx {
+class DelegatedInkMetadata;
+}  // namespace gfx
+
 namespace gl {
 
 class DirectCompositionChildSurfaceWin;
@@ -110,6 +114,9 @@
 
   bool SupportsDelegatedInk();
 
+  void SetDelegatedInkTrailStartPoint(
+      std::unique_ptr<gfx::DelegatedInkMetadata>);
+
  private:
   // This will add an ink visual to the visual tree to enable delegated ink
   // trails. This will initially always be called directly before an OS
diff --git a/ui/gl/delegated_ink_point_renderer_gpu.h b/ui/gl/delegated_ink_point_renderer_gpu.h
index ebf0bab..459dfc7 100644
--- a/ui/gl/delegated_ink_point_renderer_gpu.h
+++ b/ui/gl/delegated_ink_point_renderer_gpu.h
@@ -8,7 +8,11 @@
 #include <dcomp.h>
 #include <wrl/client.h>
 
+#include <memory>
+#include <utility>
+
 #include "base/trace_event/trace_event.h"
+#include "ui/gfx/delegated_ink_metadata.h"
 
 // Required for SFINAE to check if these Win10 types exist.
 // TODO(1171374): Remove this when the types are available in the Win10 SDK.
@@ -44,6 +48,11 @@
       const Microsoft::WRL::ComPtr<IDCompositionDevice2>& dcomp_device) const {
     return false;
   }
+
+  void SetDelegatedInkTrailStartPoint(
+      std::unique_ptr<gfx::DelegatedInkMetadata> metadata) {
+    NOTREACHED();
+  }
 };
 
 // This class will call the OS delegated ink trail APIs when they become
@@ -71,6 +80,14 @@
   bool Initialize(
       const Microsoft::WRL::ComPtr<IDCompositionDevice2>& dcomp_device2,
       const Microsoft::WRL::ComPtr<IDXGISwapChain1>& root_swap_chain) {
+    if (dcomp_device_ == dcomp_device2.Get() &&
+        swap_chain_ == root_swap_chain.Get() && HasBeenInitialized()) {
+      return true;
+    }
+
+    dcomp_device_ = dcomp_device2.Get();
+    swap_chain_ = root_swap_chain.Get();
+
     Microsoft::WRL::ComPtr<InkTrailDevice> ink_trail_device;
     TraceEventOnFailure(dcomp_device2.As(&ink_trail_device),
                         "DelegatedInkPointRendererGpu::Initialize - "
@@ -126,6 +143,16 @@
     return hr == S_OK;
   }
 
+  void SetDelegatedInkTrailStartPoint(
+      std::unique_ptr<gfx::DelegatedInkMetadata> metadata) {
+    TRACE_EVENT1("gpu",
+                 "DelegatedInkPointRendererGpu::SetDelegatedInkTrailStartPoint",
+                 "metadata", metadata->ToString());
+    metadata_ = std::move(metadata);
+    // TODO(1052145): Start using OS APIs to inform of the starting point of the
+    // ink trail.
+  }
+
  private:
   // Note that this returns true if the HRESULT is anything other than S_OK,
   // meaning that it returns true when an event is traced (because of a
@@ -145,6 +172,17 @@
   // The delegated ink trail object that the ink trail is drawn on. This is the
   // content of the ink visual.
   Microsoft::WRL::ComPtr<DelegatedInkTrail> delegated_ink_trail_;
+
+  // The most recent metadata received. The metadata marks the last point of
+  // the app rendered stroke, which corresponds to the first point of the
+  // delegated ink trail that will be drawn.
+  std::unique_ptr<gfx::DelegatedInkMetadata> metadata_;
+
+  // Remember the dcomp device and swap chain used to create
+  // |delegated_ink_trail_| and |ink_visual_| so that we can avoid recreating
+  // them when it isn't necessary.
+  IDCompositionDevice2* dcomp_device_ = nullptr;
+  IDXGISwapChain1* swap_chain_ = nullptr;
 };
 
 }  // namespace gl
diff --git a/ui/gl/direct_composition_surface_win.cc b/ui/gl/direct_composition_surface_win.cc
index 119ffb4..4ac0e1d 100644
--- a/ui/gl/direct_composition_surface_win.cc
+++ b/ui/gl/direct_composition_surface_win.cc
@@ -896,6 +896,11 @@
   return layer_tree_->SupportsDelegatedInk();
 }
 
+void DirectCompositionSurfaceWin::SetDelegatedInkTrailStartPoint(
+    std::unique_ptr<gfx::DelegatedInkMetadata> metadata) {
+  layer_tree_->SetDelegatedInkTrailStartPoint(std::move(metadata));
+}
+
 scoped_refptr<base::TaskRunner>
 DirectCompositionSurfaceWin::GetWindowTaskRunnerForTesting() {
   return child_window_.GetTaskRunnerForTesting();
diff --git a/ui/gl/direct_composition_surface_win.h b/ui/gl/direct_composition_surface_win.h
index 79cbaa28..01e7bfe 100644
--- a/ui/gl/direct_composition_surface_win.h
+++ b/ui/gl/direct_composition_surface_win.h
@@ -19,6 +19,10 @@
 #include "ui/gl/gpu_switching_observer.h"
 #include "ui/gl/vsync_observer.h"
 
+namespace gfx {
+class DelegatedInkMetadata;
+}  // namespace gfx
+
 namespace gl {
 class DCLayerTree;
 class DirectCompositionChildSurfaceWin;
@@ -157,6 +161,8 @@
   void OnDisplayMetricsChanged() override;
 
   bool SupportsDelegatedInk() override;
+  void SetDelegatedInkTrailStartPoint(
+      std::unique_ptr<gfx::DelegatedInkMetadata> metadata) override;
 
   HWND window() const { return window_; }
 
diff --git a/ui/gl/gl_surface.cc b/ui/gl/gl_surface.cc
index 6ed239b..c180260 100644
--- a/ui/gl/gl_surface.cc
+++ b/ui/gl/gl_surface.cc
@@ -4,6 +4,8 @@
 
 #include "ui/gl/gl_surface.h"
 
+#include <utility>
+
 #include "base/check.h"
 #include "base/command_line.h"
 #include "base/lazy_instance.h"
@@ -545,6 +547,11 @@
   return surface_->SupportsDelegatedInk();
 }
 
+void GLSurfaceAdapter::SetDelegatedInkTrailStartPoint(
+    std::unique_ptr<gfx::DelegatedInkMetadata> metadata) {
+  surface_->SetDelegatedInkTrailStartPoint(std::move(metadata));
+}
+
 GLSurfaceAdapter::~GLSurfaceAdapter() = default;
 
 scoped_refptr<GLSurface> InitializeGLSurfaceWithFormat(
diff --git a/ui/gl/gl_surface.h b/ui/gl/gl_surface.h
index 1f8c3d8..6aa6a8dd 100644
--- a/ui/gl/gl_surface.h
+++ b/ui/gl/gl_surface.h
@@ -13,6 +13,7 @@
 #include "base/memory/ref_counted.h"
 #include "base/memory/weak_ptr.h"
 #include "build/build_config.h"
+#include "ui/gfx/delegated_ink_metadata.h"
 #include "ui/gfx/geometry/rect.h"
 #include "ui/gfx/geometry/rect_f.h"
 #include "ui/gfx/geometry/size.h"
@@ -322,6 +323,8 @@
   static bool ExtensionsContain(const char* extensions, const char* name);
 
   virtual bool SupportsDelegatedInk();
+  virtual void SetDelegatedInkTrailStartPoint(
+      std::unique_ptr<gfx::DelegatedInkMetadata> metadata) {}
 
  protected:
   virtual ~GLSurface();
@@ -419,6 +422,8 @@
   bool IsCurrent() override;
 
   bool SupportsDelegatedInk() override;
+  void SetDelegatedInkTrailStartPoint(
+      std::unique_ptr<gfx::DelegatedInkMetadata> metadata) override;
 
   GLSurface* surface() const { return surface_.get(); }
 
diff --git a/ui/gtk/BUILD.gn b/ui/gtk/BUILD.gn
index 042d04ef..4cb6e70 100644
--- a/ui/gtk/BUILD.gn
+++ b/ui/gtk/BUILD.gn
@@ -20,11 +20,26 @@
   flags = [ "GTK_VERSION=$gtk_version" ]
 }
 
-generate_stubs("gdk_pixbuf") {
-  sigs = [ "gdk_pixbuf.sigs" ]
-  extra_header = "gdk_pixbuf.fragment"
-  output_name = "gdk_pixbuf"
-  deps = [ "//build/config/linux/gtk" ]
+source_set("gtk_types") {
+  sources = [ "gtk_types.h" ]
+  deps = [ ":gtk_buildflags" ]
+}
+
+generate_stubs("gtk_stubs") {
+  sigs = [
+    "gdk_pixbuf.sigs",
+    "gdk.sigs",
+    "gsk.sigs",
+    "gtk.sigs",
+  ]
+  extra_header = "gtk.fragment"
+  output_name = "gtk_stubs"
+  deps = [
+    ":gtk_types",
+    "//build/config/linux/gtk",
+  ]
+  logging_function = "LogNoop()"
+  logging_include = "ui/gtk/log_noop.h"
 }
 
 component("gtk_ui_delegate") {
@@ -41,6 +56,8 @@
 component("gtk") {
   public = [ "gtk_ui.h" ]
   sources = [
+    "gtk_compat.cc",
+    "gtk_compat.h",
     "gtk_ui.cc",
     "gtk_util.cc",
     "gtk_util.h",
@@ -85,7 +102,8 @@
   }
 
   deps = [
-    ":gdk_pixbuf",
+    ":gtk_stubs",
+    ":gtk_types",
     "//base",
     "//build/config/linux/gtk:gtkprint",
     "//printing",
diff --git a/ui/gtk/gdk.sigs b/ui/gtk/gdk.sigs
new file mode 100644
index 0000000..a3fbb82
--- /dev/null
+++ b/ui/gtk/gdk.sigs
@@ -0,0 +1 @@
+GdkDisplay* gdk_display_get_default(void);
diff --git a/ui/gtk/gdk_pixbuf.fragment b/ui/gtk/gdk_pixbuf.fragment
deleted file mode 100644
index 99e4ea2..0000000
--- a/ui/gtk/gdk_pixbuf.fragment
+++ /dev/null
@@ -1 +0,0 @@
-#include <gdk-pixbuf/gdk-pixbuf.h>
diff --git a/ui/gtk/gsk.sigs b/ui/gtk/gsk.sigs
new file mode 100644
index 0000000..4da2500
--- /dev/null
+++ b/ui/gtk/gsk.sigs
@@ -0,0 +1 @@
+GskRenderNodeType gsk_render_node_get_node_type(GskRenderNode *node);
diff --git a/ui/gtk/gtk.fragment b/ui/gtk/gtk.fragment
new file mode 100644
index 0000000..dffac36
--- /dev/null
+++ b/ui/gtk/gtk.fragment
@@ -0,0 +1,5 @@
+#include <gdk-pixbuf/gdk-pixbuf.h>
+#include <gdk/gdk.h>
+#include <gtk/gtk.h>
+
+#include "ui/gtk/gtk_types.h"
diff --git a/ui/gtk/gtk.sigs b/ui/gtk/gtk.sigs
new file mode 100644
index 0000000..7a50c3e
--- /dev/null
+++ b/ui/gtk/gtk.sigs
@@ -0,0 +1 @@
+GtkSettings* gtk_settings_get_default(void);
diff --git a/ui/gtk/gtk_compat.cc b/ui/gtk/gtk_compat.cc
new file mode 100644
index 0000000..5e618a1
--- /dev/null
+++ b/ui/gtk/gtk_compat.cc
@@ -0,0 +1,116 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "ui/gtk/gtk_compat.h"
+
+#include <dlfcn.h>
+#include <gtk/gtk.h>
+
+#include "base/check.h"
+#include "base/compiler_specific.h"
+#include "base/debug/leak_annotations.h"
+#include "ui/gtk/gtk_stubs.h"
+
+namespace gtk {
+
+// IMPORTANT: All functions in this file that call dlsym()'ed
+// functions should be annotated with DISABLE_CFI_ICALL.
+
+namespace {
+
+void* DlOpen(const char* library_name) {
+  void* library = dlopen(library_name, RTLD_LAZY | RTLD_GLOBAL);
+  CHECK(library);
+  return library;
+}
+
+void* DlSym(void* library, const char* name) {
+  void* symbol = dlsym(library, name);
+  CHECK(symbol);
+  return symbol;
+}
+
+template <typename T>
+auto DlCast(void* symbol) {
+  return reinterpret_cast<T*>(symbol);
+}
+
+void* GetLibGdkPixbuf() {
+  static void* libgdk_pixbuf = DlOpen("libgdk_pixbuf-2.0.so.0");
+  return libgdk_pixbuf;
+}
+
+void* GetLibGdk3() {
+  static void* libgdk3 = DlOpen("libgdk-3.so.0");
+  return libgdk3;
+}
+
+void* GetLibGtk3() {
+  static void* libgtk3 = DlOpen("libgtk-3.so.0");
+  return libgtk3;
+}
+
+void* GetLibGtk4() {
+  static void* libgtk4 = DlOpen("libgtk-4.so.1");
+  return libgtk4;
+}
+
+void* GetLibGtk() {
+  if (GtkCheckVersion(4))
+    return GetLibGtk4();
+  return GetLibGtk3();
+}
+
+}  // namespace
+
+bool LoadGtk(int gtk_version) {
+  if (gtk_version < 4) {
+    ui_gtk::InitializeGdk_pixbuf(GetLibGdkPixbuf());
+    ui_gtk::InitializeGdk(GetLibGdk3());
+    ui_gtk::InitializeGtk(GetLibGtk3());
+  } else {
+    // In GTK4, libgtk provides all gdk_*, gsk_*, and gtk_* symbols.
+    ui_gtk::InitializeGdk(GetLibGtk4());
+    ui_gtk::InitializeGsk(GetLibGtk4());
+    ui_gtk::InitializeGtk(GetLibGtk4());
+  }
+  return true;
+}
+
+bool GtkCheckVersion(int major, int minor, int micro) {
+  static auto version =
+      std::make_tuple(gtk_get_major_version(), gtk_get_minor_version(),
+                      gtk_get_micro_version());
+  return version >= std::make_tuple(major, minor, micro);
+}
+
+DISABLE_CFI_ICALL
+void GtkInit(const std::vector<std::string>& args) {
+  static void* gtk_init = DlSym(GetLibGtk(), "gtk_init");
+  if (GtkCheckVersion(4)) {
+    DlCast<void()>(gtk_init)();
+  } else {
+    // gtk_init() modifies argv, so make a copy first.
+    size_t args_chars = 0;
+    for (const auto& arg : args)
+      args_chars += arg.size() + 1;
+    std::vector<char> args_copy(args_chars);
+    std::vector<char*> argv;
+    char* dst = args_copy.data();
+    for (const auto& arg : args) {
+      argv.push_back(strcpy(dst, arg.c_str()));
+      dst += arg.size() + 1;
+    }
+
+    int gtk_argc = argv.size();
+    char** gtk_argv = argv.data();
+    {
+      // http://crbug.com/423873
+      ANNOTATE_SCOPED_MEMORY_LEAK;
+      DlCast<void(int*, char***)>(gtk_init)(&gtk_argc, &gtk_argv);
+    }
+  }
+}
+
+}  // namespace gtk
diff --git a/ui/gtk/gtk_compat.h b/ui/gtk/gtk_compat.h
new file mode 100644
index 0000000..fb509a7
--- /dev/null
+++ b/ui/gtk/gtk_compat.h
@@ -0,0 +1,28 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef UI_GTK_GTK_COMPAT_H_
+#define UI_GTK_GTK_COMPAT_H_
+
+#include <string>
+#include <vector>
+
+namespace gtk {
+
+// Loads libgtk and related libraries and returns true on success.
+bool LoadGtk(int gtk_version);
+
+// Returns true iff the runtime version of Gtk used meets
+// |major|.|minor|.|micro|. LoadGtk() must have been called
+// prior to calling this function.
+bool GtkCheckVersion(int major, int minor = 0, int micro = 0);
+
+// The below functions replace GTK functions whose interface has
+// changed across versions, but whose (symbol) names have not.
+
+void GtkInit(const std::vector<std::string>& args);
+
+}  // namespace gtk
+
+#endif
diff --git a/ui/gtk/gtk_types.h b/ui/gtk/gtk_types.h
new file mode 100644
index 0000000..22aa098
--- /dev/null
+++ b/ui/gtk/gtk_types.h
@@ -0,0 +1,20 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef UI_GTK_GTK_TYPES_H_
+#define UI_GTK_GTK_TYPES_H_
+
+#include "ui/gtk/gtk_buildflags.h"
+
+// This file provides types that are only available in specific versions of GTK.
+
+extern "C" {
+#if BUILDFLAG(GTK_VERSION) == 3
+using GskRenderNode = struct _GskRenderNode;
+enum GskRenderNodeType : int;
+GskRenderNodeType gsk_render_node_get_node_type(GskRenderNode* node);
+#endif
+}
+
+#endif  // UI_GTK_GTK_TYPES_H_
diff --git a/ui/gtk/gtk_ui.cc b/ui/gtk/gtk_ui.cc
index 8765fa62..73bc393 100644
--- a/ui/gtk/gtk_ui.cc
+++ b/ui/gtk/gtk_ui.cc
@@ -46,7 +46,7 @@
 #include "ui/gfx/image/image_skia_source.h"
 #include "ui/gfx/skbitmap_operations.h"
 #include "ui/gfx/skia_util.h"
-#include "ui/gtk/gdk_pixbuf.h"
+#include "ui/gtk/gtk_compat.h"
 #include "ui/gtk/gtk_ui_delegate.h"
 #include "ui/gtk/gtk_util.h"
 #include "ui/gtk/input_method_context_impl_gtk.h"
@@ -79,7 +79,7 @@
 #include "printing/printing_context_linux.h"
 #endif
 
-#if !GTK_CHECK_VERSION(3, 90, 0)
+#if BUILDFLAG(GTK_VERSION) < 4
 #include "ui/gtk/gtk_key_bindings_handler.h"
 #endif
 
@@ -145,7 +145,7 @@
     if (focus_) {
       gfx::Rect focus_rect(width, height);
 
-#if !GTK_CHECK_VERSION(3, 90, 0)
+#if BUILDFLAG(GTK_VERSION) < 4
       if (!GtkCheckVersion(3, 14)) {
         gint focus_pad;
         gtk_style_context_get_style(context, "focus-padding", &focus_pad,
@@ -167,7 +167,7 @@
 
       if (!GtkCheckVersion(3, 20)) {
         GtkBorder border;
-#if GTK_CHECK_VERSION(3, 90, 0)
+#if BUILDFLAG(GTK_VERSION) >= 4
         gtk_style_context_get_border(context, &border);
 #else
         gtk_style_context_get_border(context, state_flags, &border);
@@ -311,6 +311,9 @@
       {ActionSource::kMiddleClick, GetDefaultMiddleClickAction()},
       {ActionSource::kRightClick, Action::kMenu}};
 
+  static bool loaded = LoadGtk(BUILDFLAG(GTK_VERSION));
+  CHECK(loaded);
+
   // Avoid GTK initializing atk-bridge, and let AuraLinux implementation
   // do it once it is ready.
   std::unique_ptr<base::Environment> env(base::Environment::Create());
@@ -342,9 +345,6 @@
   }
 #endif
 
-  CHECK(ui_gtk::InitializeStubs(
-      {{ui_gtk::kModuleGdk_pixbuf, {"libgdk_pixbuf-2.0.so.0"}}}));
-
   GtkSettings* settings = gtk_settings_get_default();
   g_signal_connect_after(settings, "notify::gtk-theme-name",
                          G_CALLBACK(OnThemeChangedThunk), this);
@@ -503,7 +503,7 @@
 
   for (size_t i = 0; i < base::size(content_types); ++i) {
     auto icon = TakeGObject(g_content_type_get_icon(content_type.c_str()));
-#if GTK_CHECK_VERSION(3, 98, 0)
+#if BUILDFLAG(GTK_VERSION) >= 4
     auto icon_paintable = TakeGObject(gtk_icon_theme_lookup_by_gicon(
         theme, icon.get(), size, 1, GTK_TEXT_DIR_NONE,
         static_cast<GtkIconLookupFlags>(0)));
@@ -710,7 +710,7 @@
 
 base::flat_map<std::string, std::string> GtkUi::GetKeyboardLayoutMap() {
   GdkDisplay* display = gdk_display_get_default();
-#if !GTK_CHECK_VERSION(3, 90, 0)
+#if BUILDFLAG(GTK_VERSION) < 4
   GdkKeymap* keymap = gdk_keymap_get_for_display(display);
   if (!keymap)
     return {};
@@ -730,7 +730,7 @@
 // The order of the layouts is based on the system default ordering in
 // Keyboard Settings. The currently active layout does not affect this
 // order.
-#if GTK_CHECK_VERSION(3, 98, 3)
+#if BUILDFLAG(GTK_VERSION) >= 4
     const bool success =
         gdk_display_map_keycode(display, keycode, &keys, &keyvals, &n_entries);
 #else
@@ -781,7 +781,7 @@
 
 bool GtkUi::MatchEvent(const ui::Event& event,
                        std::vector<ui::TextEditCommandAuraLinux>* commands) {
-#if GTK_CHECK_VERSION(3, 90, 0)
+#if BUILDFLAG(GTK_VERSION) >= 4
   // GTK4 dropped custom key bindings.
   return false;
 #else
diff --git a/ui/gtk/gtk_util.cc b/ui/gtk/gtk_util.cc
index a9ccc980..d945446 100644
--- a/ui/gtk/gtk_util.cc
+++ b/ui/gtk/gtk_util.cc
@@ -4,7 +4,6 @@
 
 #include "ui/gtk/gtk_util.h"
 
-#include <dlfcn.h>
 #include <gdk/gdk.h>
 #include <gtk/gtk.h>
 #include <locale.h>
@@ -14,7 +13,6 @@
 
 #include "base/command_line.h"
 #include "base/compiler_specific.h"
-#include "base/debug/leak_annotations.h"
 #include "base/environment.h"
 #include "base/strings/string_split.h"
 #include "base/strings/string_tokenizer.h"
@@ -49,32 +47,10 @@
   // setlocale(LC_NUMERIC, "C") by now. Chrome does this in
   // service_manager::Main.
   DCHECK_EQ(strcmp(setlocale(LC_NUMERIC, nullptr), "C"), 0);
-  // This prevent GTK from calling setlocale(LC_ALL, ""), which potentially
+  // This prevents GTK from calling setlocale(LC_ALL, ""), which potentially
   // overwrites the LC_NUMERIC locale to something other than "C".
   gtk_disable_setlocale();
-#if GTK_CHECK_VERSION(3, 90, 0)
-  gtk_init();
-#else
-  const std::vector<std::string>& args = command_line.argv();
-  int argc = args.size();
-  std::unique_ptr<char*[]> argv(new char*[argc + 1]);
-  for (size_t i = 0; i < args.size(); ++i) {
-    // TODO(piman@google.com): can gtk_init modify argv? Just being safe
-    // here.
-    argv[i] = strdup(args[i].c_str());
-  }
-  argv[argc] = nullptr;
-  char** argv_pointer = argv.get();
-
-  {
-    // http://crbug.com/423873
-    ANNOTATE_SCOPED_MEMORY_LEAK;
-    gtk_init(&argc, &argv_pointer);
-  }
-  for (size_t i = 0; i < args.size(); ++i) {
-    free(argv[i]);
-  }
-#endif
+  GtkInit(command_line.argv());
 }
 
 GdkModifierType GetIbusFlags(const ui::KeyEvent& key_event) {
@@ -292,13 +268,6 @@
                         g * 255 / a, b * 255 / a);
 }
 
-bool GtkCheckVersion(int major, int minor, int micro) {
-  static auto version =
-      std::make_tuple(gtk_get_major_version(), gtk_get_minor_version(),
-                      gtk_get_micro_version());
-  return version >= std::make_tuple(major, minor, micro);
-}
-
 #if BUILDFLAG(GTK_VERSION) >= 4
 GtkCssContext::GtkCssContext(GtkWidget* widget, GtkWidget* root)
     : widget_(widget), root_(WrapGObject(root)) {}
@@ -452,7 +421,7 @@
 
 SkColor GetFgColorFromStyleContext(GtkStyleContext* context) {
   GdkRGBA color;
-#if GTK_CHECK_VERSION(3, 90, 0)
+#if BUILDFLAG(GTK_VERSION) >= 4
   gtk_style_context_get_color(context, &color);
 #else
   gtk_style_context_get_color(context, gtk_style_context_get_state(context),
@@ -486,7 +455,7 @@
 
 ScopedCssProvider GetCssProvider(const std::string& css) {
   auto provider = TakeGObject(gtk_css_provider_new());
-#if GTK_CHECK_VERSION(3, 90, 0)
+#if BUILDFLAG(GTK_VERSION) >= 4
   gtk_css_provider_load_from_data(provider, css.c_str(), -1);
 #else
   GError* error = nullptr;
@@ -570,7 +539,7 @@
 
   int w = 1, h = 1;
   GtkBorder border, padding;
-#if GTK_CHECK_VERSION(3, 90, 0)
+#if BUILDFLAG(GTK_VERSION) >= 4
   auto size = GetSeparatorSize(horizontal);
   w = size.width();
   h = size.height();
@@ -683,7 +652,7 @@
 #endif
 
 GtkIconTheme* GetDefaultIconTheme() {
-#if GTK_CHECK_VERSION(3, 90, 0)
+#if BUILDFLAG(GTK_VERSION) >= 4
   return gtk_icon_theme_get_for_display(gdk_display_get_default());
 #else
   return gtk_icon_theme_get_default();
@@ -691,7 +660,7 @@
 }
 
 void GtkWindowDestroy(GtkWidget* widget) {
-#if GTK_CHECK_VERSION(3, 98, 4)
+#if BUILDFLAG(GTK_VERSION) >= 4
   gtk_window_destroy(GTK_WINDOW(widget));
 #else
   gtk_widget_destroy(widget);
diff --git a/ui/gtk/gtk_util.h b/ui/gtk/gtk_util.h
index 1d4ace77..c292f150 100644
--- a/ui/gtk/gtk_util.h
+++ b/ui/gtk/gtk_util.h
@@ -13,6 +13,7 @@
 
 #include "ui/base/glib/scoped_gobject.h"
 #include "ui/gtk/gtk_buildflags.h"
+#include "ui/gtk/gtk_compat.h"
 #include "ui/native_theme/native_theme.h"
 #include "ui/views/window/frame_buttons.h"
 
@@ -79,10 +80,6 @@
   cairo_t* cairo_;
 };
 
-// Returns true iff the runtime version of Gtk used meets
-// |major|.|minor|.|micro|.
-bool GtkCheckVersion(int major, int minor = 0, int micro = 0);
-
 class GtkCssContext {
  public:
   GtkCssContext();
@@ -131,7 +128,7 @@
 
 using ScopedCssProvider = ScopedGObject<GtkCssProvider>;
 
-#if !GTK_CHECK_VERSION(3, 90, 0)
+#if BUILDFLAG(GTK_VERSION) < 4
 }  // namespace gtk
 
 // Template override cannot be in the gtk namespace.
diff --git a/ui/gtk/log_noop.h b/ui/gtk/log_noop.h
new file mode 100644
index 0000000..ba7b7461
--- /dev/null
+++ b/ui/gtk/log_noop.h
@@ -0,0 +1,18 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef UI_GTK_LOG_NOOP_H_
+#define UI_GTK_LOG_NOOP_H_
+
+// This is a no-op logger used to ignore error messages reported by generated
+// stub initializers.  Some missing symbols are expected since they may only be
+// available in specific versions of GTK.
+struct LogNoop {
+  template <typename T>
+  LogNoop operator<<(const T& t) {
+    return *this;
+  }
+};
+
+#endif  // UI_GTK_LOG_NOOP_H_
diff --git a/ui/gtk/native_theme_gtk.cc b/ui/gtk/native_theme_gtk.cc
index 72a9e088..c08edc5 100644
--- a/ui/gtk/native_theme_gtk.cc
+++ b/ui/gtk/native_theme_gtk.cc
@@ -188,7 +188,7 @@
     case ui::NativeTheme::kColorId_LinkEnabled: {
       if (GtkCheckVersion(3, 12))
         return GetFgColor("GtkLabel#label.link:link");
-#if !GTK_CHECK_VERSION(3, 90, 0)
+#if BUILDFLAG(GTK_VERSION) < 4
       auto link_context = GetStyleContextFromCss("GtkLabel#label.view");
       GdkColor* color;
       gtk_style_context_get_style(link_context, "link-color", &color, nullptr);
@@ -695,7 +695,7 @@
         GetStyleContextFromCss(CSS_MENU " GtkSeparator#separator.horizontal");
     GtkBorder margin, border, padding;
     int min_height = 1;
-#if GTK_CHECK_VERSION(3, 90, 0)
+#if BUILDFLAG(GTK_VERSION) >= 4
     gtk_style_context_get_margin(context, &margin);
     gtk_style_context_get_border(context, &border);
     gtk_style_context_get_padding(context, &padding);
@@ -715,7 +715,7 @@
     int y = separator_offset(h);
     PaintWidget(canvas, gfx::Rect(x, y, w, h), context, BG_RENDER_NORMAL, true);
   } else {
-#if !GTK_CHECK_VERSION(3, 90, 0)
+#if BUILDFLAG(GTK_VERSION) < 4
     auto context = GetStyleContextFromCss(CSS_MENU " " CSS_MENUITEM
                                                    ".separator.horizontal");
     gboolean wide_separators = false;
diff --git a/ui/gtk/nav_button_provider_gtk.cc b/ui/gtk/nav_button_provider_gtk.cc
index f249d1e..c4d49dd 100644
--- a/ui/gtk/nav_button_provider_gtk.cc
+++ b/ui/gtk/nav_button_provider_gtk.cc
@@ -17,7 +17,7 @@
 
 namespace {
 
-#if GTK_CHECK_VERSION(3, 90, 0)
+#if BUILDFLAG(GTK_VERSION) >= 4
 using NavButtonIcon = ScopedGObject<GdkTexture>;
 #else
 using NavButtonIcon = ScopedGObject<GdkPixbuf>;
@@ -86,7 +86,7 @@
 gfx::Insets PaddingFromStyleContext(GtkStyleContext* context,
                                     GtkStateFlags state) {
   GtkBorder padding;
-#if GTK_CHECK_VERSION(3, 90, 0)
+#if BUILDFLAG(GTK_VERSION) >= 4
   gtk_style_context_get_padding(context, &padding);
 #else
   gtk_style_context_get_padding(context, state, &padding);
@@ -97,7 +97,7 @@
 gfx::Insets BorderFromStyleContext(GtkStyleContext* context,
                                    GtkStateFlags state) {
   GtkBorder border;
-#if GTK_CHECK_VERSION(3, 90, 0)
+#if BUILDFLAG(GTK_VERSION) >= 4
   gtk_style_context_get_border(context, &border);
 #else
   gtk_style_context_get_border(context, state, &border);
@@ -108,7 +108,7 @@
 gfx::Insets MarginFromStyleContext(GtkStyleContext* context,
                                    GtkStateFlags state) {
   GtkBorder margin;
-#if GTK_CHECK_VERSION(3, 90, 0)
+#if BUILDFLAG(GTK_VERSION) >= 4
   gtk_style_context_get_margin(context, &margin);
 #else
   gtk_style_context_get_margin(context, state, &margin);
@@ -122,7 +122,7 @@
     int scale,
     NavButtonIcon* icon = nullptr) {
   const char* icon_name = IconNameFromButtonType(type);
-#if GTK_CHECK_VERSION(3, 90, 0)
+#if BUILDFLAG(GTK_VERSION) >= 4
   auto icon_paintable = TakeGObject(gtk_icon_theme_lookup_icon(
       GetDefaultIconTheme(), icon_name, nullptr, kNavButtonIconSize, scale,
       GTK_TEXT_DIR_NONE, static_cast<GtkIconLookupFlags>(0)));
@@ -299,7 +299,7 @@
     // "contain" if clipping would occur.
     int bg_width = 0;
     int bg_height = 0;
-#if GTK_CHECK_VERSION(3, 90, 0)
+#if BUILDFLAG(GTK_VERSION) >= 4
     auto* snapshot = gtk_snapshot_new();
     gtk_snapshot_render_background(snapshot, button_context, 0, 0,
                                    button_size_.width(), button_size_.height());
diff --git a/ui/gtk/printing/print_dialog_gtk.cc b/ui/gtk/printing/print_dialog_gtk.cc
index d2ed5cb..7363dc2 100644
--- a/ui/gtk/printing/print_dialog_gtk.cc
+++ b/ui/gtk/printing/print_dialog_gtk.cc
@@ -371,7 +371,7 @@
   gtk::SetGtkTransientForAura(dialog_, parent_view);
   if (parent_view)
     parent_view->AddObserver(this);
-#if GTK_CHECK_VERSION(3, 94, 0)
+#if BUILDFLAG(GTK_VERSION) >= 4
   gtk_window_set_hide_on_close(GTK_WINDOW(dialog_), true);
 #else
   g_signal_connect(dialog_, "delete-event",
diff --git a/ui/gtk/select_file_dialog_impl_gtk.cc b/ui/gtk/select_file_dialog_impl_gtk.cc
index 3d50df1..517fc52 100644
--- a/ui/gtk/select_file_dialog_impl_gtk.cc
+++ b/ui/gtk/select_file_dialog_impl_gtk.cc
@@ -38,7 +38,7 @@
 
 namespace {
 
-#if GTK_CHECK_VERSION(3, 90, 0)
+#if BUILDFLAG(GTK_VERSION) >= 4
 // TODO(https://crbug.com/981309): These getters will be unnecessary after
 // migrating to GtkFileChooserNative.
 const char* GettextPackage() {
@@ -90,7 +90,7 @@
 
 void GtkFileChooserSetCurrentFolder(GtkFileChooser* dialog,
                                     const base::FilePath& path) {
-#if GTK_CHECK_VERSION(3, 90, 0)
+#if BUILDFLAG(GTK_VERSION) >= 4
   auto file = TakeGObject(g_file_new_for_path(path.value().c_str()));
   gtk_file_chooser_set_current_folder(dialog, file, nullptr);
 #else
@@ -100,7 +100,7 @@
 
 void GtkFileChooserSetFilename(GtkFileChooser* dialog,
                                const base::FilePath& path) {
-#if GTK_CHECK_VERSION(3, 90, 0)
+#if BUILDFLAG(GTK_VERSION) >= 4
   auto file = TakeGObject(g_file_new_for_path(path.value().c_str()));
   gtk_file_chooser_set_file(dialog, file, nullptr);
 #else
@@ -111,7 +111,7 @@
 int GtkDialogSelectedFilterIndex(GtkWidget* dialog) {
   GtkFileFilter* selected_filter =
       gtk_file_chooser_get_filter(GTK_FILE_CHOOSER(dialog));
-#if GTK_CHECK_VERSION(3, 90, 0)
+#if BUILDFLAG(GTK_VERSION) >= 4
   auto filters =
       TakeGObject(gtk_file_chooser_get_filters(GTK_FILE_CHOOSER(dialog)));
   int size = g_list_model_get_n_items(filters);
@@ -130,7 +130,7 @@
 
 std::string GtkFileChooserGetFilename(GtkWidget* dialog) {
   const char* filename = nullptr;
-#if GTK_CHECK_VERSION(3, 90, 0)
+#if BUILDFLAG(GTK_VERSION) >= 4
   auto file = TakeGObject(gtk_file_chooser_get_file(GTK_FILE_CHOOSER(dialog)));
   if (file)
     filename = g_file_peek_path(file);
@@ -147,7 +147,7 @@
 
 std::vector<base::FilePath> GtkFileChooserGetFilenames(GtkWidget* dialog) {
   std::vector<base::FilePath> filenames_fp;
-#if GTK_CHECK_VERSION(3, 90, 0)
+#if BUILDFLAG(GTK_VERSION) >= 4
   auto files =
       TakeGObject(gtk_file_chooser_get_files(GTK_FILE_CHOOSER(dialog)));
   auto size = g_list_model_get_n_items(files);
@@ -173,7 +173,7 @@
 
 namespace gtk {
 
-#if !GTK_CHECK_VERSION(3, 90, 0)
+#if BUILDFLAG(GTK_VERSION) < 4
 // The size of the preview we display for selected image files. We set height
 // larger than width because generally there is more free space vertically
 // than horiztonally (setting the preview image will always expand the width of
@@ -275,7 +275,7 @@
       NOTREACHED();
       return;
   }
-#if GTK_CHECK_VERSION(3, 90, 0)
+#if BUILDFLAG(GTK_VERSION) >= 4
   gtk_window_set_hide_on_close(GTK_WINDOW(dialog), true);
 #else
   g_signal_connect(dialog, "delete-event",
@@ -285,7 +285,7 @@
   dialogs_[dialog] = g_signal_connect(
       dialog, "destroy", G_CALLBACK(OnFileChooserDestroyThunk), this);
 
-#if !GTK_CHECK_VERSION(3, 90, 0)
+#if BUILDFLAG(GTK_VERSION) < 4
   preview_ = gtk_image_new();
   g_signal_connect(dialog, "update-preview", G_CALLBACK(OnUpdatePreviewThunk),
                    this);
@@ -316,7 +316,7 @@
     }
   }
 
-#if !GTK_CHECK_VERSION(3, 90, 0)
+#if BUILDFLAG(GTK_VERSION) < 4
   gtk_widget_show_all(dialog);
 #endif
   gtk::GtkUi::GetDelegate()->ShowGtkWindow(GTK_WINDOW(dialog));
@@ -544,7 +544,7 @@
   }
   gtk_file_chooser_set_select_multiple(GTK_FILE_CHOOSER(dialog), FALSE);
   // Overwrite confirmation is always enabled in GTK4.
-#if !GTK_CHECK_VERSION(3, 90, 0)
+#if BUILDFLAG(GTK_VERSION) < 4
   gtk_file_chooser_set_do_overwrite_confirmation(GTK_FILE_CHOOSER(dialog),
                                                  TRUE);
 #endif
@@ -647,7 +647,7 @@
   }
 }
 
-#if !GTK_CHECK_VERSION(3, 90, 0)
+#if BUILDFLAG(GTK_VERSION) < 4
 void SelectFileDialogImplGTK::OnUpdatePreview(GtkWidget* chooser) {
   gchar* filename =
       gtk_file_chooser_get_preview_filename(GTK_FILE_CHOOSER(chooser));
diff --git a/ui/gtk/select_file_dialog_impl_gtk.h b/ui/gtk/select_file_dialog_impl_gtk.h
index 8e0e99d..7ad893a 100644
--- a/ui/gtk/select_file_dialog_impl_gtk.h
+++ b/ui/gtk/select_file_dialog_impl_gtk.h
@@ -124,7 +124,7 @@
                      OnFileChooserDestroy,
                      GtkWidget*);
 
-#if !GTK_CHECK_VERSION(3, 90, 0)
+#if BUILDFLAG(GTK_VERSION) < 4
   // Callback for when we update the preview for the selection.
   CHROMEG_CALLBACK_0(SelectFileDialogImplGTK,
                      void,
@@ -136,7 +136,7 @@
   std::map<GtkWidget*, void*> params_map_;
 
   // GTK4 provides its own preview.
-#if !GTK_CHECK_VERSION(3, 90, 0)
+#if BUILDFLAG(GTK_VERSION) < 4
   // The GtkImage widget for showing previews of selected images.
   GtkWidget* preview_ = nullptr;
 #endif
diff --git a/ui/gtk/settings_provider_gtk.cc b/ui/gtk/settings_provider_gtk.cc
index 31df0cf..04a3c1e0 100644
--- a/ui/gtk/settings_provider_gtk.cc
+++ b/ui/gtk/settings_provider_gtk.cc
@@ -13,7 +13,7 @@
 namespace {
 
 std::string GetDecorationLayoutFromGtkWindow() {
-#if GTK_CHECK_VERSION(3, 90, 0)
+#if BUILDFLAG(GTK_VERSION) >= 4
   NOTREACHED();
   static const char kDefaultGtkLayout[] = "menu:minimize,maximize,close";
   return kDefaultGtkLayout;
diff --git a/ui/gtk/x/gtk_event_loop_x11.cc b/ui/gtk/x/gtk_event_loop_x11.cc
index 7e1c01dc..5300739 100644
--- a/ui/gtk/x/gtk_event_loop_x11.cc
+++ b/ui/gtk/x/gtk_event_loop_x11.cc
@@ -12,7 +12,7 @@
 #include "ui/gfx/x/event.h"
 
 extern "C" {
-#if GTK_CHECK_VERSION(3, 90, 0)
+#if BUILDFLAG(GTK_VERSION) >= 4
 unsigned long gdk_x11_surface_get_xid(GdkSurface* surface);
 #else
 unsigned long gdk_x11_window_get_xid(GdkWindow* window);
@@ -29,7 +29,7 @@
 }
 
 x11::KeyEvent ConvertGdkEventToKeyEvent(GdkEvent* gdk_event) {
-#if GTK_CHECK_VERSION(3, 90, 0)
+#if BUILDFLAG(GTK_VERSION) >= 4
   GdkKeymapKey* keys = nullptr;
   guint* keyvals = nullptr;
   gint n_entries = 0;
@@ -94,7 +94,7 @@
   // corresponding key event in the X event queue.  So we have to handle this
   // case.  ibus-gtk is used through gtk-immodule to support IMEs.
 
-#if GTK_CHECK_VERSION(3, 90, 0)
+#if BUILDFLAG(GTK_VERSION) >= 4
   auto event_type = gdk_event_get_event_type(gdk_event);
 #else
   auto event_type = gdk_event->type;
diff --git a/ui/ozone/platform/wayland/host/gtk_ui_delegate_wayland.cc b/ui/ozone/platform/wayland/host/gtk_ui_delegate_wayland.cc
index 37a49fb..729085f 100644
--- a/ui/ozone/platform/wayland/host/gtk_ui_delegate_wayland.cc
+++ b/ui/ozone/platform/wayland/host/gtk_ui_delegate_wayland.cc
@@ -20,7 +20,7 @@
 #include "ui/ozone/platform/wayland/host/xdg_foreign_wrapper.h"
 #include "ui/platform_window/platform_window_init_properties.h"
 
-#if GTK_CHECK_VERSION(3, 90, 0)
+#if BUILDFLAG(GTK_VERSION) >= 4
 #include <gdk/wayland/gdkwayland.h>
 #else
 #include <gdk/gdkwayland.h>
@@ -61,7 +61,7 @@
 bool GtkUiDelegateWayland::SetGtkWidgetTransientFor(
     GtkWidget* widget,
     gfx::AcceleratedWidget parent) {
-#if !GTK_CHECK_VERSION(3, 90, 0)
+#if BUILDFLAG(GTK_VERSION) < 4
   if (!gdk_wayland_window_set_transient_for_exported) {
     LOG(WARNING) << "set_transient_for_exported not supported in GTK version "
                  << GTK_MAJOR_VERSION << '.' << GTK_MINOR_VERSION << '.'
@@ -102,7 +102,7 @@
 void GtkUiDelegateWayland::OnHandle(GtkWidget* widget,
                                     const std::string& handle) {
   char* parent = const_cast<char*>(handle.c_str());
-#if GTK_CHECK_VERSION(3, 90, 0)
+#if BUILDFLAG(GTK_VERSION) >= 4
   auto* toplevel =
       GDK_TOPLEVEL(gtk_native_get_surface(gtk_widget_get_native(widget)));
   gdk_wayland_toplevel_set_transient_for_exported(toplevel, parent);