[graphite][vulkan] Support VK_ext_frame_boundary extension

Extended queue submission interface to take a struct similar
to Ganesh that would allow extra metadata to be passed during
submission. Added support to mark frame boundary using
VK_ext_frame_boundary extension if supported. This will make
tracing of Skia-based Android applications easier as some
of them do not use common frame boundary mechanisms like
presenting image on a swapchain

Bug: b/439531864
Change-Id: Ife15d64442de24a691f6213583b18c9f7f38a4bf
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/1055416
Commit-Queue: Max Kolesin <maxkolesin@google.com>
Reviewed-by: Nicolette Prevost <nicolettep@google.com>
Reviewed-by: Michael Ludwig <michaelludwig@google.com>
diff --git a/include/gpu/graphite/Context.h b/include/gpu/graphite/Context.h
index dfcc734..2fd8be1 100644
--- a/include/gpu/graphite/Context.h
+++ b/include/gpu/graphite/Context.h
@@ -80,7 +80,7 @@
     std::unique_ptr<PrecompileContext> makePrecompileContext();
 
     InsertStatus insertRecording(const InsertRecordingInfo&);
-    bool submit(SyncToCpu = SyncToCpu::kNo);
+    bool submit(SubmitInfo submitInfo = {});
 
     /** Returns true if there is work that was submitted to the GPU that has not finished. */
     bool hasUnfinishedGpuWork() const;
diff --git a/include/gpu/graphite/GraphiteTypes.h b/include/gpu/graphite/GraphiteTypes.h
index 2940096..a638da8 100644
--- a/include/gpu/graphite/GraphiteTypes.h
+++ b/include/gpu/graphite/GraphiteTypes.h
@@ -170,6 +170,29 @@
     kNo = false
 };
 
+enum class MarkFrameBoundary : bool {
+    kYes = true,
+    kNo = false
+};
+
+struct SubmitInfo {
+    SyncToCpu fSync = SyncToCpu::kNo;
+    MarkFrameBoundary fMarkBoundary = MarkFrameBoundary::kNo;
+    uint64_t fFrameID = 0;
+
+    constexpr SubmitInfo() = default;
+
+    constexpr SubmitInfo(SyncToCpu sync)
+        : fSync(sync)
+        , fMarkBoundary(MarkFrameBoundary::kNo)
+        , fFrameID(0) {}
+
+    constexpr SubmitInfo(SyncToCpu sync, uint64_t frameID)
+        : fSync(sync)
+        , fMarkBoundary(MarkFrameBoundary::kYes)
+        , fFrameID(frameID) {}
+};
+
 /*
  * For Promise Images - should the Promise Image be fulfilled every time a Recording that references
  * it is inserted into the Context.
diff --git a/include/gpu/vk/VulkanPreferredFeatures.h b/include/gpu/vk/VulkanPreferredFeatures.h
index cbe88ac..3810a42 100644
--- a/include/gpu/vk/VulkanPreferredFeatures.h
+++ b/include/gpu/vk/VulkanPreferredFeatures.h
@@ -189,6 +189,9 @@
     // Feature of VK_EXT_pipeline_creation_cache_control or Vulkan 1.3
     VkPhysicalDevicePipelineCreationCacheControlFeatures fPipelineCreationCacheControl = {};
 
+    // Feature of VK_EXT_frame_boundary
+    VkPhysicalDeviceFrameBoundaryFeaturesEXT fFrameBoundary = {};
+
     // Extensions that don't have a feature:
     // VK_KHR_driver_properties or Vulkan 1.2
     const char* fDriverPropertiesExtension = nullptr;
diff --git a/relnotes/graphite_submitinfo.md b/relnotes/graphite_submitinfo.md
new file mode 100644
index 0000000..f0511da
--- /dev/null
+++ b/relnotes/graphite_submitinfo.md
@@ -0,0 +1,5 @@
+Add enum class `skgpu::graphite::MarkFrameBoundary` to be used to specify whether a submission is the last logical submission for a frame.
+
+Add struct `skgpu::graphite::SubmitInfo` to hold metadata used for submitting workloads for execution. Allow specifying through `skgpu::graphite::SubmitInfo` whether submission is a frame boundary (last logical submission for a frame) and frameID (uint64_t) with default values to match prior behavior.
+
+Use struct `skgpu::graphite::SubmitInfo` in `skgpu::graphite::QueueManager::submitToGpu`, `skgpu::graphite::QueueManager::onSubmitToGpu` and all derived classes.
diff --git a/src/gpu/graphite/Context.cpp b/src/gpu/graphite/Context.cpp
index 6e34e75..11f611e 100644
--- a/src/gpu/graphite/Context.cpp
+++ b/src/gpu/graphite/Context.cpp
@@ -181,7 +181,7 @@
         return false;
     }
     if (result == StaticBufferManager::FinishResult::kSuccess &&
-        !fQueueManager->submitToGpu()) {
+        !fQueueManager->submitToGpu(/*submitInfo=*/{})) {
         SKGPU_LOG_W("Failed to submit initial command buffer for Context creation.\n");
         return false;
     } // else result was kNoWork so skip submitting to the GPU
@@ -238,16 +238,16 @@
     return fQueueManager->addRecording(info, this);
 }
 
-bool Context::submit(SyncToCpu syncToCpu) {
+bool Context::submit(SubmitInfo submitInfo) {
     ASSERT_SINGLE_OWNER
 
-    if (syncToCpu == SyncToCpu::kYes && !fSharedContext->caps()->allowCpuSync()) {
+    if (submitInfo.fSync == SyncToCpu::kYes && !fSharedContext->caps()->allowCpuSync()) {
         SKGPU_LOG_E("SyncToCpu::kYes not supported with ContextOptions::fNeverYieldToWebGPU. "
                     "The parameter is ignored and no synchronization will occur.");
-        syncToCpu = SyncToCpu::kNo;
+        submitInfo.fSync = SyncToCpu::kNo;
     }
-    bool success = fQueueManager->submitToGpu();
-    this->checkForFinishedWork(syncToCpu);
+    bool success = fQueueManager->submitToGpu(submitInfo);
+    this->checkForFinishedWork(submitInfo.fSync);
     return success;
 }
 
diff --git a/src/gpu/graphite/QueueManager.cpp b/src/gpu/graphite/QueueManager.cpp
index 756c56b..2948e07 100644
--- a/src/gpu/graphite/QueueManager.cpp
+++ b/src/gpu/graphite/QueueManager.cpp
@@ -274,7 +274,7 @@
     return true;
 }
 
-bool QueueManager::submitToGpu() {
+bool QueueManager::submitToGpu(const SubmitInfo& submitInfo) {
     TRACE_EVENT0_ALWAYS("skia.gpu", TRACE_FUNC);
 
     if (!fCurrentCommandBuffer) {
@@ -291,7 +291,7 @@
     }
 #endif
 
-    auto submission = this->onSubmitToGpu();
+    auto submission = this->onSubmitToGpu(submitInfo);
     if (!submission) {
         return false;
     }
diff --git a/src/gpu/graphite/QueueManager.h b/src/gpu/graphite/QueueManager.h
index 8464566..4c380ce 100644
--- a/src/gpu/graphite/QueueManager.h
+++ b/src/gpu/graphite/QueueManager.h
@@ -63,7 +63,7 @@
                                      ResourceProvider*,
                                      SkSpan<const sk_sp<Buffer>> buffersToAsyncMap = {});
 
-    [[nodiscard]] bool submitToGpu();
+    [[nodiscard]] bool submitToGpu(const SubmitInfo&);
     [[nodiscard]] bool hasUnfinishedGpuWork();
     void checkForFinishedWork(SyncToCpu);
 
@@ -88,7 +88,7 @@
 
 private:
     virtual std::unique_ptr<CommandBuffer> getNewCommandBuffer(ResourceProvider*, Protected) = 0;
-    virtual OutstandingSubmission onSubmitToGpu() = 0;
+    virtual OutstandingSubmission onSubmitToGpu(const SubmitInfo&) = 0;
 
     bool setupCommandBuffer(ResourceProvider*, Protected);
 
diff --git a/src/gpu/graphite/dawn/DawnQueueManager.cpp b/src/gpu/graphite/dawn/DawnQueueManager.cpp
index 6d30ba6..a2a7ee2 100644
--- a/src/gpu/graphite/dawn/DawnQueueManager.cpp
+++ b/src/gpu/graphite/dawn/DawnQueueManager.cpp
@@ -113,7 +113,7 @@
                                    static_cast<DawnResourceProvider*>(resourceProvider));
 }
 
-QueueManager::OutstandingSubmission DawnQueueManager::onSubmitToGpu() {
+QueueManager::OutstandingSubmission DawnQueueManager::onSubmitToGpu(const SubmitInfo&) {
     SkASSERT(fCurrentCommandBuffer);
     DawnCommandBuffer* dawnCmdBuffer = static_cast<DawnCommandBuffer*>(fCurrentCommandBuffer.get());
     auto wgpuCmdBuffer = dawnCmdBuffer->finishEncoding();
diff --git a/src/gpu/graphite/dawn/DawnQueueManager.h b/src/gpu/graphite/dawn/DawnQueueManager.h
index 1e1928e..f1cfcef 100644
--- a/src/gpu/graphite/dawn/DawnQueueManager.h
+++ b/src/gpu/graphite/dawn/DawnQueueManager.h
@@ -30,7 +30,7 @@
     const DawnSharedContext* dawnSharedContext() const;
 
     std::unique_ptr<CommandBuffer> getNewCommandBuffer(ResourceProvider*, Protected) override;
-    OutstandingSubmission onSubmitToGpu() override;
+    OutstandingSubmission onSubmitToGpu(const SubmitInfo&) override;
 
 #if defined(GPU_TEST_UTILS)
     void startCapture() override;
diff --git a/src/gpu/graphite/mtl/MtlQueueManager.h b/src/gpu/graphite/mtl/MtlQueueManager.h
index ff084fb..e2b3b74 100644
--- a/src/gpu/graphite/mtl/MtlQueueManager.h
+++ b/src/gpu/graphite/mtl/MtlQueueManager.h
@@ -26,7 +26,7 @@
     const MtlSharedContext* mtlSharedContext() const;
 
     std::unique_ptr<CommandBuffer> getNewCommandBuffer(ResourceProvider*, Protected) override;
-    OutstandingSubmission onSubmitToGpu() override;
+    OutstandingSubmission onSubmitToGpu(const SubmitInfo&) override;
 
 #if defined(GPU_TEST_UTILS)
     void startCapture() override;
diff --git a/src/gpu/graphite/mtl/MtlQueueManager.mm b/src/gpu/graphite/mtl/MtlQueueManager.mm
index c86c794..e51a49e 100644
--- a/src/gpu/graphite/mtl/MtlQueueManager.mm
+++ b/src/gpu/graphite/mtl/MtlQueueManager.mm
@@ -49,7 +49,7 @@
     }
 };
 
-QueueManager::OutstandingSubmission MtlQueueManager::onSubmitToGpu() {
+QueueManager::OutstandingSubmission MtlQueueManager::onSubmitToGpu(const SubmitInfo&) {
     SkASSERT(fCurrentCommandBuffer);
     MtlCommandBuffer* mtlCmdBuffer = static_cast<MtlCommandBuffer*>(fCurrentCommandBuffer.get());
     if (!mtlCmdBuffer->commit()) {
diff --git a/src/gpu/graphite/vk/VulkanCaps.cpp b/src/gpu/graphite/vk/VulkanCaps.cpp
index fd06f15..2f600e5 100644
--- a/src/gpu/graphite/vk/VulkanCaps.cpp
+++ b/src/gpu/graphite/vk/VulkanCaps.cpp
@@ -207,6 +207,7 @@
 
     fSupportsYcbcrConversion = enabledFeatures.fSamplerYcbcrConversion;
     fSupportsDeviceFaultInfo = enabledFeatures.fDeviceFault;
+    fSupportsFrameBoundary = enabledFeatures.fFrameBoundary;
 
     if (enabledFeatures.fAdvancedBlendModes) {
         fBlendEqSupport = enabledFeatures.fCoherentAdvancedBlendModes
@@ -252,6 +253,9 @@
             enabledFeatures.fGraphicsPipelineLibrary &&
             (deviceProperties.fGpl.graphicsPipelineLibraryFastLinking || vendorID == kARM_VkVendor);
 
+
+    fSupportsFrameBoundary = enabledFeatures.fFrameBoundary;
+
     // Multisampled render to single-sampled usage depends on the mandatory feature of
     // VK_EXT_multisampled_render_to_single_sampled.  Per format queries are needed to determine if
     // multisampled->single-sampled rendering is supported, which should in practice always be equal
@@ -386,6 +390,13 @@
                     enabled.fHostImageCopy = feature->hostImageCopy;
                     break;
                 }
+                case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FRAME_BOUNDARY_FEATURES_EXT: {
+                    const auto *feature = reinterpret_cast<
+                            const VkPhysicalDeviceFrameBoundaryFeaturesEXT*>(
+                            pNext);
+                    enabled.fFrameBoundary = feature->frameBoundary;
+                    break;
+                }
                 default:
                     break;
             }
diff --git a/src/gpu/graphite/vk/VulkanCaps.h b/src/gpu/graphite/vk/VulkanCaps.h
index 49f293c1..bb6bed5 100644
--- a/src/gpu/graphite/vk/VulkanCaps.h
+++ b/src/gpu/graphite/vk/VulkanCaps.h
@@ -119,6 +119,8 @@
     bool mustLoadFullImageForMSAA() const { return fMustLoadFullImageForMSAA; }
     bool avoidMSAA() const { return fAvoidMSAA; }
 
+    bool supportsFrameBoundary() const { return fSupportsFrameBoundary; }
+
 private:
     void init(const ContextOptions&,
               const skgpu::VulkanInterface*,
@@ -152,6 +154,8 @@
         bool fMultisampledRenderToSingleSampled = false;
         // From VkPhysicalDeviceHostImageCopyFeatures:
         bool fHostImageCopy = false;
+        // From VkPhysicalDeviceFrameBoundaryFeaturesEXT:
+        bool fFrameBoundary = false;
     };
     EnabledFeatures getEnabledFeatures(const VkPhysicalDeviceFeatures2*,
                                        uint32_t physicalDeviceVersion);
@@ -322,6 +326,7 @@
     bool fSupportsDeviceFaultInfo = false;
     bool fSupportsRasterizationOrderColorAttachmentAccess = false;
     bool fIsInputAttachmentReadCoherent = false;
+    bool fSupportsFrameBoundary = false;
 
     // Flags to enable workarounds for driver bugs
     bool fMustLoadFullImageForMSAA = false;
diff --git a/src/gpu/graphite/vk/VulkanCommandBuffer.cpp b/src/gpu/graphite/vk/VulkanCommandBuffer.cpp
index 31a7bde..41a1872 100644
--- a/src/gpu/graphite/vk/VulkanCommandBuffer.cpp
+++ b/src/gpu/graphite/vk/VulkanCommandBuffer.cpp
@@ -299,16 +299,32 @@
                                 const VkCommandBuffer* commandBuffers,
                                 uint32_t signalCount,
                                 const VkSemaphore* signalSemaphores,
-                                Protected protectedContext) {
+                                Protected protectedContext,
+                                MarkFrameBoundary markFrameBoundary,
+                                uint64_t frameID) {
+    VkSubmitInfo submitInfo = {};
+    submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
+
     VkProtectedSubmitInfo protectedSubmitInfo = {};
     if (protectedContext == Protected::kYes) {
         protectedSubmitInfo.sType = VK_STRUCTURE_TYPE_PROTECTED_SUBMIT_INFO;
         protectedSubmitInfo.protectedSubmit = VK_TRUE;
+
+        AddToPNextChain(&submitInfo, &protectedSubmitInfo);
     }
 
-    VkSubmitInfo submitInfo = {};
-    submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
-    submitInfo.pNext = protectedContext == Protected::kYes ? &protectedSubmitInfo : nullptr;
+
+    VkFrameBoundaryEXT frameBoundary;
+    if (markFrameBoundary == MarkFrameBoundary::kYes &&
+        sharedContext->vulkanCaps().supportsFrameBoundary()) {
+        memset(&frameBoundary, 0, sizeof(VkFrameBoundaryEXT));
+        frameBoundary.sType = VK_STRUCTURE_TYPE_FRAME_BOUNDARY_EXT;
+        frameBoundary.flags = VK_FRAME_BOUNDARY_FRAME_END_BIT_EXT;
+        frameBoundary.frameID = frameID;
+
+        AddToPNextChain(&submitInfo, &frameBoundary);
+    }
+
     submitInfo.waitSemaphoreCount = waitCount;
     submitInfo.pWaitSemaphores = waitSemaphores;
     submitInfo.pWaitDstStageMask = waitStages;
@@ -316,12 +332,13 @@
     submitInfo.pCommandBuffers = commandBuffers;
     submitInfo.signalSemaphoreCount = signalCount;
     submitInfo.pSignalSemaphores = signalSemaphores;
+
     VkResult result;
     VULKAN_CALL_RESULT(sharedContext, result, QueueSubmit(queue, 1, &submitInfo, fence));
     return result;
 }
 
-bool VulkanCommandBuffer::submit(VkQueue queue) {
+bool VulkanCommandBuffer::submit(VkQueue queue, const SubmitInfo& submitInfo) {
     this->end();
 
     auto device = fSharedContext->device();
@@ -360,7 +377,9 @@
                                             &fPrimaryCommandBuffer,
                                             fSignalSemaphores.size(),
                                             fSignalSemaphores.data(),
-                                            this->isProtected());
+                                            this->isProtected(),
+                                            submitInfo.fMarkBoundary,
+                                            submitInfo.fFrameID);
     fWaitSemaphores.clear();
     fSignalSemaphores.clear();
     if (submitResult != VK_SUCCESS) {
diff --git a/src/gpu/graphite/vk/VulkanCommandBuffer.h b/src/gpu/graphite/vk/VulkanCommandBuffer.h
index a9b51db..40445b1 100644
--- a/src/gpu/graphite/vk/VulkanCommandBuffer.h
+++ b/src/gpu/graphite/vk/VulkanCommandBuffer.h
@@ -32,7 +32,7 @@
 
     bool setNewCommandBufferResources() override;
 
-    bool submit(VkQueue);
+    bool submit(VkQueue, const SubmitInfo&);
 
     bool isFinished();
 
diff --git a/src/gpu/graphite/vk/VulkanQueueManager.cpp b/src/gpu/graphite/vk/VulkanQueueManager.cpp
index ceebb76..a36f31e 100644
--- a/src/gpu/graphite/vk/VulkanQueueManager.cpp
+++ b/src/gpu/graphite/vk/VulkanQueueManager.cpp
@@ -49,11 +49,11 @@
     }
 };
 
-QueueManager::OutstandingSubmission VulkanQueueManager::onSubmitToGpu() {
+QueueManager::OutstandingSubmission VulkanQueueManager::onSubmitToGpu(const SubmitInfo& submitInfo) {
     SkASSERT(fCurrentCommandBuffer);
     VulkanCommandBuffer* vkCmdBuffer =
             static_cast<VulkanCommandBuffer*>(fCurrentCommandBuffer.get());
-    if (!vkCmdBuffer->submit(fQueue)) {
+    if (!vkCmdBuffer->submit(fQueue, submitInfo)) {
         fCurrentCommandBuffer->callFinishedProcs(/*success=*/false);
         return nullptr;
     }
diff --git a/src/gpu/graphite/vk/VulkanQueueManager.h b/src/gpu/graphite/vk/VulkanQueueManager.h
index ba123d8..6330b9c 100644
--- a/src/gpu/graphite/vk/VulkanQueueManager.h
+++ b/src/gpu/graphite/vk/VulkanQueueManager.h
@@ -25,7 +25,7 @@
     const VulkanSharedContext* vkSharedContext() const;
 
     std::unique_ptr<CommandBuffer> getNewCommandBuffer(ResourceProvider*, Protected) override;
-    OutstandingSubmission onSubmitToGpu() override;
+    OutstandingSubmission onSubmitToGpu(const SubmitInfo& submitInfo) override;
 
 #if defined(GPU_TEST_UTILS)
     // TODO: Implement these
diff --git a/src/gpu/vk/VulkanPreferredFeatures.cpp b/src/gpu/vk/VulkanPreferredFeatures.cpp
index 05b3cfb..b412263 100644
--- a/src/gpu/vk/VulkanPreferredFeatures.cpp
+++ b/src/gpu/vk/VulkanPreferredFeatures.cpp
@@ -159,6 +159,7 @@
     //    VK_EXT_sampler_filter_minmax          samplerFilterMinmax
     //    VK_EXT_shader_viewport_index_layer    shaderOutputViewportIndex, shaderOutputLayer
     //    VK_KHR_push_descriptor                pushDescriptor
+    //    VK_EXT_frame_boundary                 frameBoundaryEXT
     bool fShaderDrawParametersKHR = false;
     bool fDrawIndirectCountKHR = false;
     bool fSamplerMirrorClampToEdgeKHR = false;
@@ -166,6 +167,7 @@
     bool fSamplerFilterMinmaxEXT = false;
     bool fShaderViewportIndexLayerEXT = false;
     bool fPushDescriptorKHR = false;
+    bool fFrameBoundaryEXT = false;
 };
 
 void mark_device_extensions(DeviceExtensions& exts, const char* name) {
@@ -231,6 +233,8 @@
         exts.fShaderViewportIndexLayerEXT = true;
     } else if (strcmp(name, VK_KHR_PUSH_DESCRIPTOR_EXTENSION_NAME) == 0) {
         exts.fPushDescriptorKHR = true;
+    } else if (strcmp(name, VK_EXT_FRAME_BOUNDARY_EXTENSION_NAME) == 0) {
+        exts.fFrameBoundaryEXT = true;
     }
 }
 
@@ -264,6 +268,7 @@
     bool fMultisampledRenderToSingleSampled = true;
     bool fHostImageCopy = true;
     bool fPipelineCreationCacheControl = true;
+    bool fFrameBoundary = true;
 };
 
 FeaturesToAdd get_features_to_add(uint32_t apiVersion,
@@ -331,6 +336,7 @@
     toAdd.fGraphicsPipelineLibrary = exts.fGraphicsPipelineLibraryEXT;
     toAdd.fRGBA10x6Formats = exts.fRGBA10x6FormatsEXT;
     toAdd.fMultisampledRenderToSingleSampled = exts.fMultisampledRenderToSingleSampledEXT;
+    toAdd.fFrameBoundary = exts.fFrameBoundaryEXT;
 
     // Then, go over appFeatures::pNext and exclude features that are already being queried by the
     // app.
@@ -391,6 +397,9 @@
             case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PIPELINE_CREATION_CACHE_CONTROL_FEATURES:
                 toAdd.fPipelineCreationCacheControl = false;
                 break;
+            case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FRAME_BOUNDARY_FEATURES_EXT:
+                toAdd.fFrameBoundary = false;
+                break;
             default:
                 break;
         }
@@ -569,6 +578,9 @@
     if (exts.fConservativeRasterizationEXT) {
         fConservativeRasterizationExtension = VK_EXT_CONSERVATIVE_RASTERIZATION_EXTENSION_NAME;
     }
+    if (exts.fFrameBoundaryEXT) {
+        fFrameBoundary.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FRAME_BOUNDARY_FEATURES_EXT;
+    }
 
     // Inspect the list of features the app has already included and decide on which features to
     // add.
@@ -647,6 +659,10 @@
         SkASSERT(fPipelineCreationCacheControl.sType != 0);
         AddToPNextChain(&appFeatures, &fPipelineCreationCacheControl);
     }
+    if (toAdd.fFrameBoundary) {
+        SkASSERT(fFrameBoundary.sType != 0);
+        AddToPNextChain(&appFeatures, &fFrameBoundary);
+    }
 
     fHasAddedFeaturesToQuery = true;
 }
@@ -679,6 +695,7 @@
     toAdd.fMultisampledRenderToSingleSampled = fMultisampledRenderToSingleSampled.sType != 0;
     toAdd.fHostImageCopy = fHostImageCopy.sType != 0;
     toAdd.fPipelineCreationCacheControl = fPipelineCreationCacheControl.sType != 0;
+    toAdd.fFrameBoundary = fFrameBoundary.sType != 0;
 
     // Check which extensions are already enabled by the application.
     DeviceExtensions exts;
@@ -744,6 +761,10 @@
         appExtensions.push_back(VK_EXT_PIPELINE_CREATION_CACHE_CONTROL_EXTENSION_NAME);
         exts.fPipelineCreationCacheControlEXT = true;
     }
+    if (!exts.fFrameBoundaryEXT && toAdd.fFrameBoundary) {
+        appExtensions.push_back(VK_EXT_FRAME_BOUNDARY_EXTENSION_NAME);
+        exts.fFrameBoundaryEXT = true;
+    }
     if (!exts.fDriverPropertiesKHR && fDriverPropertiesExtension != nullptr) {
         appExtensions.push_back(fDriverPropertiesExtension);
         exts.fDriverPropertiesKHR = true;
@@ -1763,6 +1784,17 @@
                 }
                 break;
             }
+            case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FRAME_BOUNDARY_FEATURES_EXT: {
+                chain(newChainEnd, pNext);
+                if (exts.fFrameBoundaryEXT) {
+                    auto* features =
+                            reinterpret_cast<VkPhysicalDeviceFrameBoundaryFeaturesEXT*>(
+                                    pNext);
+                    features->frameBoundary = VK_TRUE;
+                }
+                toAdd.fFrameBoundary = false;
+                break;
+            }
 
             default:
                 // Retain everything else that's chained.
@@ -1829,6 +1861,9 @@
     if (toAdd.fPipelineCreationCacheControl) {
         chain(newChainEnd, &fPipelineCreationCacheControl);
     }
+    if (toAdd.fFrameBoundary) {
+        chain(newChainEnd, &fFrameBoundary);
+    }
 
     // Replace appFeatures' pNext with the new chain.
     *newChainEnd = nullptr;
diff --git a/tests/VkPreferredFeaturesTest.cpp b/tests/VkPreferredFeaturesTest.cpp
index 668cb75..0e3c963 100644
--- a/tests/VkPreferredFeaturesTest.cpp
+++ b/tests/VkPreferredFeaturesTest.cpp
@@ -59,6 +59,7 @@
     bool fSamplerFilterMinmaxEXT = true;
     bool fShaderViewportIndexLayerEXT = true;
     bool fPushDescriptorKHR = true;
+    bool fFrameBoundaryEXT = true;
 };
 
 static std::vector<VkExtensionProperties> get_device_exts(const VulkanExts& config) {
@@ -103,6 +104,7 @@
     ADD_EXT(fSamplerFilterMinmaxEXT, VK_EXT_SAMPLER_FILTER_MINMAX);
     ADD_EXT(fShaderViewportIndexLayerEXT, VK_EXT_SHADER_VIEWPORT_INDEX_LAYER);
     ADD_EXT(fPushDescriptorKHR, VK_KHR_PUSH_DESCRIPTOR);
+    ADD_EXT(fFrameBoundaryEXT, VK_EXT_FRAME_BOUNDARY);
 #undef ADD_EXT
     return exts;
 }
@@ -219,6 +221,10 @@
                          PipelineCreationCacheControlFeatures,
                          SET_IF(fPipelineCreationCacheControlEXT, pipelineCreationCacheControl))
 
+            SET_FEATURES(FRAME_BOUNDARY_FEATURES_EXT,
+                         FrameBoundaryFeaturesEXT,
+                         SET_IF(fFrameBoundaryEXT, frameBoundary))
+
             default:
                 break;
         }
@@ -401,6 +407,7 @@
     CHECK_EXT_DISABLED(VK_EXT_SAMPLER_FILTER_MINMAX);
     CHECK_EXT_DISABLED(VK_EXT_SHADER_VIEWPORT_INDEX_LAYER);
     CHECK_EXT_DISABLED(VK_KHR_PUSH_DESCRIPTOR);
+    CHECK_EXT_ENABLED(VK_EXT_FRAME_BOUNDARY);
 
     CHECK_FEATURE_ENABLED(RASTERIZATION_ORDER_ATTACHMENT_ACCESS_FEATURES_EXT,
                           RasterizationOrderAttachmentAccessFeaturesEXT,
@@ -463,6 +470,10 @@
     CHECK_FEATURE_ENABLED(PIPELINE_CREATION_CACHE_CONTROL_FEATURES,
                           PipelineCreationCacheControlFeatures,
                           pipelineCreationCacheControl);
+
+    CHECK_FEATURE_ENABLED(FRAME_BOUNDARY_FEATURES_EXT,
+                          FrameBoundaryFeaturesEXT,
+                          frameBoundary);
 }
 
 // A test where the application adds no features and extensions and lets Skia choose them. Uses
@@ -527,6 +538,7 @@
     CHECK_EXT_DISABLED(VK_EXT_SAMPLER_FILTER_MINMAX);
     CHECK_EXT_DISABLED(VK_EXT_SHADER_VIEWPORT_INDEX_LAYER);
     CHECK_EXT_DISABLED(VK_KHR_PUSH_DESCRIPTOR);
+    CHECK_EXT_ENABLED(VK_EXT_FRAME_BOUNDARY);
 
     CHECK_EXCLUSIVE(VULKAN_1_1_FEATURES, 16BIT_STORAGE_FEATURES);
     CHECK_EXCLUSIVE(VULKAN_1_1_FEATURES, MULTIVIEW_FEATURES);
@@ -619,6 +631,10 @@
     CHECK_FEATURE_ENABLED(PIPELINE_CREATION_CACHE_CONTROL_FEATURES,
                           PipelineCreationCacheControlFeatures,
                           pipelineCreationCacheControl);
+
+    CHECK_FEATURE_ENABLED(FRAME_BOUNDARY_FEATURES_EXT,
+                          FrameBoundaryFeaturesEXT,
+                          frameBoundary);
 }
 
 // A test where the application adds no features and extensions and lets Skia choose them. Uses
@@ -683,6 +699,7 @@
     CHECK_EXT_DISABLED(VK_EXT_SAMPLER_FILTER_MINMAX);
     CHECK_EXT_DISABLED(VK_EXT_SHADER_VIEWPORT_INDEX_LAYER);
     CHECK_EXT_DISABLED(VK_KHR_PUSH_DESCRIPTOR);
+    CHECK_EXT_ENABLED(VK_EXT_FRAME_BOUNDARY);
 
     CHECK_EXCLUSIVE(VULKAN_1_1_FEATURES, 16BIT_STORAGE_FEATURES);
     CHECK_EXCLUSIVE(VULKAN_1_1_FEATURES, MULTIVIEW_FEATURES);
@@ -790,6 +807,10 @@
     CHECK_FEATURE_DISABLED(PIPELINE_CREATION_CACHE_CONTROL_FEATURES,
                            PipelineCreationCacheControlFeatures,
                            pipelineCreationCacheControl);
+
+    CHECK_FEATURE_ENABLED(FRAME_BOUNDARY_FEATURES_EXT,
+                          FrameBoundaryFeaturesEXT,
+                          frameBoundary);
 }
 
 // A test where the application adds no features and extensions and lets Skia choose them. Uses
@@ -854,6 +875,7 @@
     CHECK_EXT_DISABLED(VK_EXT_SAMPLER_FILTER_MINMAX);
     CHECK_EXT_DISABLED(VK_EXT_SHADER_VIEWPORT_INDEX_LAYER);
     CHECK_EXT_DISABLED(VK_KHR_PUSH_DESCRIPTOR);
+    CHECK_EXT_ENABLED(VK_EXT_FRAME_BOUNDARY);
 
     CHECK_EXCLUSIVE(VULKAN_1_1_FEATURES, 16BIT_STORAGE_FEATURES);
     CHECK_EXCLUSIVE(VULKAN_1_1_FEATURES, MULTIVIEW_FEATURES);
@@ -976,6 +998,10 @@
     CHECK_FEATURE_DISABLED(PIPELINE_CREATION_CACHE_CONTROL_FEATURES,
                            PipelineCreationCacheControlFeatures,
                            pipelineCreationCacheControl);
+
+    CHECK_FEATURE_ENABLED(FRAME_BOUNDARY_FEATURES_EXT,
+                          FrameBoundaryFeaturesEXT,
+                          frameBoundary);
 }
 
 #define CHAIN(STRUCT_NAME, StructName, Feature)                              \
@@ -1003,6 +1029,7 @@
     config.fGraphicsPipelineLibraryEXT = false;
     config.fExtendedDynamicState2EXT = false;
     config.fCreateRenderpass2KHR = false;
+    config.fFrameBoundaryEXT = false;
 
     const std::vector<VkExtensionProperties> exts = get_device_exts(config);
 
@@ -1074,6 +1101,7 @@
     CHECK_EXT_ENABLED(VK_EXT_SUBGROUP_SIZE_CONTROL);
     CHECK_EXT_ENABLED(VK_KHR_MAINTENANCE_4);
     CHECK_EXT_ENABLED(VK_KHR_PUSH_DESCRIPTOR);
+    CHECK_EXT_DISABLED(VK_EXT_FRAME_BOUNDARY);
 
     CHECK_FEATURE_ENABLED(RASTERIZATION_ORDER_ATTACHMENT_ACCESS_FEATURES_EXT,
                           RasterizationOrderAttachmentAccessFeaturesEXT,
@@ -1141,6 +1169,10 @@
     CHECK_FEATURE_ENABLED(SHADER_OBJECT_FEATURES_EXT, ShaderObjectFeaturesEXT, shaderObject);
     CHECK_FEATURE_ENABLED(MAINTENANCE_5_FEATURES, Maintenance5Features, maintenance5);
     CHECK_FEATURE_ENABLED(HOST_IMAGE_COPY_FEATURES, HostImageCopyFeatures, hostImageCopy);
+
+    CHECK_FEATURE_DISABLED(FRAME_BOUNDARY_FEATURES_EXT,
+                          FrameBoundaryFeaturesEXT,
+                          frameBoundary);
 }
 
 // A test where the application adds some features and extensions and lets Skia add to them. At the
@@ -1154,6 +1186,7 @@
     config.fVertexInputDynamicStateEXT = false;
     config.fExtendedDynamicStateEXT = false;
     config.fExtendedDynamicState2EXT = false;
+    config.fFrameBoundaryEXT = false;
 
     const std::vector<VkExtensionProperties> exts = get_device_exts(config);
 
@@ -1237,6 +1270,7 @@
     CHECK_EXT_ENABLED(VK_KHR_SHADER_DRAW_PARAMETERS);
     CHECK_EXT_ENABLED(VK_EXT_DESCRIPTOR_INDEXING);
     CHECK_EXT_ENABLED(VK_KHR_SAMPLER_MIRROR_CLAMP_TO_EDGE);
+    CHECK_EXT_DISABLED(VK_EXT_FRAME_BOUNDARY);
 
     CHECK_FEATURE_ENABLED(VULKAN_1_1_FEATURES, Vulkan11Features, samplerYcbcrConversion);
     CHECK_FEATURE_ENABLED(VULKAN_1_1_FEATURES, Vulkan11Features, shaderDrawParameters);
@@ -1351,6 +1385,10 @@
     CHECK_FEATURE_ENABLED(PIPELINE_CREATION_CACHE_CONTROL_FEATURES,
                           PipelineCreationCacheControlFeatures,
                           pipelineCreationCacheControl);
+
+    CHECK_FEATURE_DISABLED(FRAME_BOUNDARY_FEATURES_EXT,
+                          FrameBoundaryFeaturesEXT,
+                          frameBoundary);
 }
 
 // A test where the application adds some features and extensions and lets Skia add to them. At the
@@ -1459,6 +1497,7 @@
     CHECK_EXT_ENABLED(VK_KHR_SHADER_DRAW_PARAMETERS);
     CHECK_EXT_ENABLED(VK_EXT_DESCRIPTOR_INDEXING);
     CHECK_EXT_ENABLED(VK_KHR_DYNAMIC_RENDERING);
+    CHECK_EXT_ENABLED(VK_EXT_FRAME_BOUNDARY);
 
     CHECK_FEATURE_ENABLED(VULKAN_1_1_FEATURES, Vulkan11Features, multiview);
     CHECK_FEATURE_DISABLED(VULKAN_1_1_FEATURES, Vulkan11Features, samplerYcbcrConversion);
@@ -1562,6 +1601,10 @@
 
     // Features enabled by the app must remain enabled
     CHECK_FEATURE_ENABLED(SHADER_OBJECT_FEATURES_EXT, ShaderObjectFeaturesEXT, shaderObject);
+
+    CHECK_FEATURE_ENABLED(FRAME_BOUNDARY_FEATURES_EXT,
+                          FrameBoundaryFeaturesEXT,
+                          frameBoundary);
 }
 
 // A test where the application adds some features and extensions and lets Skia add to them. At the
@@ -1660,6 +1703,7 @@
     CHECK_EXT_ENABLED(VK_EXT_SHADER_OBJECT);
     CHECK_EXT_ENABLED(VK_EXT_DESCRIPTOR_INDEXING);
     CHECK_EXT_ENABLED(VK_KHR_DYNAMIC_RENDERING_LOCAL_READ);
+    CHECK_EXT_ENABLED(VK_EXT_FRAME_BOUNDARY);
 
     CHECK_FEATURE_ENABLED(VULKAN_1_1_FEATURES, Vulkan11Features, samplerYcbcrConversion);
     CHECK_FEATURE_DISABLED(VULKAN_1_1_FEATURES, Vulkan11Features, shaderDrawParameters);
@@ -1715,6 +1759,11 @@
 
     // Features enabled by the app must remain enabled
     CHECK_FEATURE_ENABLED(SHADER_OBJECT_FEATURES_EXT, ShaderObjectFeaturesEXT, shaderObject);
+
+
+    CHECK_FEATURE_ENABLED(FRAME_BOUNDARY_FEATURES_EXT,
+                          FrameBoundaryFeaturesEXT,
+                          frameBoundary);
 }
 
 // A similar test to VkPreferredFeaturesTest_CustomVulkan*, except the chain passed to
@@ -1726,6 +1775,7 @@
     config.fGraphicsPipelineLibraryEXT = false;
     config.fPipelineLibraryKHR = false;
     config.fBlendOperationAdvancedEXT = false;
+    config.fFrameBoundaryEXT = false;
 
     const std::vector<VkExtensionProperties> exts = get_device_exts(config);
 
@@ -1811,6 +1861,7 @@
     CHECK_EXT_ENABLED(VK_KHR_MAINTENANCE_5);
     CHECK_EXT_ENABLED(VK_KHR_PUSH_DESCRIPTOR);
     CHECK_EXT_ENABLED(VK_EXT_SHADER_OBJECT);
+    CHECK_EXT_DISABLED(VK_EXT_FRAME_BOUNDARY);
 
     CHECK_FEATURE_ENABLED(VULKAN_1_1_FEATURES, Vulkan11Features, samplerYcbcrConversion);
     CHECK_FEATURE_DISABLED(VULKAN_1_1_FEATURES, Vulkan11Features, shaderDrawParameters);
@@ -1864,6 +1915,10 @@
     // Features enabled by the app must remain enabled
     CHECK_FEATURE_ENABLED(SHADER_OBJECT_FEATURES_EXT, ShaderObjectFeaturesEXT, shaderObject);
     CHECK_FEATURE_ENABLED(MAINTENANCE_5_FEATURES, Maintenance5Features, maintenance5);
+
+    CHECK_FEATURE_DISABLED(FRAME_BOUNDARY_FEATURES_EXT,
+                           FrameBoundaryFeaturesEXT,
+                           frameBoundary);
 }
 
 #endif