/*
 * Copyright 2025 Google Inc.
 *
 * Use of this source code is governed by a BSD-style license that can be
 * found in the LICENSE file.
 */

#include "include/gpu/vk/VulkanPreferredFeatures.h"

#include "include/private/base/SkAssert.h"
#include "include/private/base/SkDebug.h"
#include "src/gpu/vk/VulkanUtilsPriv.h"

#include <cstdint>
#include <cstring>
#include <vector>

namespace skgpu {

/*
 * # Overview
 *
 * Skia requires the application to create the Vulkan instance and device. This operation involves
 * enabling extensions and device features that the application would like to take advantage of.
 * However, the application is not necessarily aware of all the extensions and features that _Skia_
 * would like to use (and it's too onerous to require them to keep up). If the application does not
 * enable some extensions, Skia's performance may degrade.
 *
 * VulkanPreferredFeatures is used to let Skia modify the list of extensions and device features
 * before the application creates the device. The application thus never needs to worry about what
 * Skia might or might not use.
 *
 * # How It Works
 *
 * There are three main operations:
 *
 * - Add instance extensions. Before creating the Vulkan instance, Skia could add more extensions to
 *   enable.
 * - Add device features to query. This lets Skia discover what features are available.
 * - Enable device features.
 *
 * Device features and extensions are where the complexity is. In particular, there is:
 *
 * - The list of _available_ extensions. When adding features to query, this information is
 *   assembled in DeviceExtensions for faster look up.
 * - The list of _enabled_ extensions. When enabling features, this information is also assembled in
 *   DeviceExtensions.
 * - The list of device features. This is a chain of VkPhysicalDeviceFooFeatures structs, starting
 *   with VkPhysicalDeviceFeatures2. FeaturesToAdd is used to track whether any of these feature
 *   structs need to be chained or not. The choice of feature structs depends on the API version
 *   used by the application, following the extension promotions to core.
 *
 * See addFeaturesToQuery() for details of how feature structs are chosen and chained to be queried
 * in addition to the application's own set of feature structs.
 *
 * See addFeaturesToEnable() for details of how the application-provided chain of feature structs is
 * manipulated to include the previously-queried features. Based on Vulkan rules, some structs are
 * mutually exclusive, which is handled by this function as well.
 *
 * # How to Add Support for New Extensions
 *
 * When adding a new extension, the following changes must be made:
 *
 * - Add a flag corresponding to the extension to DeviceExtensions
 *   - Detect the extension and set it in mark_device_extensions().
 * - Add the extensions's feature struct to VulkanPreferredFeatures. This provides storage for the
 *   struct so it lives long enough (and not go out of scope too early).
 * - Add a flag corresponding to the feature struct to FeaturesToAdd.
 *   - In addFeaturesToQuery(), initalize the sType of the feature struct based on the flag in
 *     DeviceExtensions. If extension is promoted to core in the API version used by the
 *     application, the VkPhysicalDeviceVulkan??Features struct should be used for the same feature
 *     instead, if available.
 *   - In get_features_to_add(), if the extension is promoted to a core version, set the
 *     FeaturesToAdd flag to false as the VkPhysicalDeviceVulkan??Features struct would be used to
 *     query the same feature.
 *   - In get_features_to_add() additionally, set the FeaturesToAdd flag to false if the application
 *     has already included the corresponding feature struct in its own chain.
 *   - Back in addFeaturesToQuery(), chain the feature struct if the FeaturesToAdd flag is set.
 * - In addFeaturesToEnable():
 *   - Set the FeaturesToAdd flag to true iff the sType of the feature struct is set.
 *   - If the extension is not enabled by the app but the FeaturesToAdd flag is set, add the
 *     extension to appExtensions.
 *   - Disable optional features in the struct if not going to be used by Skia. Note that this
 *     modifies the struct owned by VulkanPreferredFeatures; if that's the one that's chained, the
 *     application didn't care about these features. If the application has chained its own struct
 *     of this type, it's unaffected by this operation.
 *   - Detect the struct in the big switch. If promoted to core, make sure to drop the struct. If
 *     not, chain it if the extension is enabled.
 *   - In the end, chain the feature struct if still needed to be added.
 * - Make sure to update the existing tests in tests/VkPreferredFeaturesTest.cpp to make sure the
 *   extension and its feature is enabled appropropriately.
 *
 * # How to Add Support for New Vulkan Versions
 *
 * When adding a support for a new Vulkan version, the following changes must be made:
 *
 * - Add the VkPhysicalDeviceVulkan??Features struct to VulkanPreferredFeatures, and a flag for it
 *   in FeaturesToAdd.
 * - In get_features_to_add(), check for the Vulkan version and decided if that struct should be
 *   used, or the individual feature structs from extensions that were promoted to core in that
 *   version.
 *   - Check in the same function if the application has already included
 *     VkPhysicalDeviceVulkan??Features.
 * - In addFeaturesToQuery(), similarly check for the Vulkan version and set the sType of either the
 *   VkPhysicalDeviceVulkan??Features struct or the individual feature structs from promoted
 *   extensions.
 *   - Chain the VkPhysicalDeviceVulkan??Features struct if the FeaturesToAdd flag is set.
 * - In addFeaturesToEnable():
 *   - Set the FeaturesToAdd flag to true iff the sType of the feature struct is set.
 *   - Disable optional features in the struct if not going to be used by Skia.
 *   - Detect the struct in the big switch. If pointer does not point to VulkanPreferredFeatures's
 *     own struct, copy the application-enabled features to VulkanPreferredFeatures's struct. Note
 *     that for simplicity, Skia always chains its own instance of VkPhysicalDeviceVulkan??Features.
 *   - Based on valid usage that forbids including VkPhysicalDeviceVulkan??Features and feature
 *     structs from promoted extensions, detect the latter structs in the big switch, copy their
 *     features to VkPhysicalDeviceVulkan??Features and drop them from the chain.
 *   - In the end, chain the VkPhysicalDeviceVulkan??Features feature struct.
 * - Make sure to update tests/VkPreferredFeaturesTest.cpp with a new test that ensures the
 *   promotion rules were followed.
 */

namespace {
struct DeviceExtensions {
    // Extensions that Skia may benefit from enabling if available.
    bool fRasterizationOrderAttachmentAccessARM = false;
    bool fRasterizationOrderAttachmentAccessEXT = false;
    bool fBlendOperationAdvancedEXT = false;
    bool fExtendedDynamicStateEXT = false;
    bool fExtendedDynamicState2EXT = false;
    bool fVertexInputDynamicStateEXT = false;
    bool fGraphicsPipelineLibraryEXT = false;
    bool fSamplerYcbcrConversionKHR = false;
    bool fRGBA10x6FormatsEXT = false;
    bool fSynchronization2KHR = false;
    bool fDynamicRenderingKHR = false;
    bool fDynamicRenderingLocalReadKHR = false;
    bool fMultisampledRenderToSingleSampledEXT = false;
    bool fHostImageCopyEXT = false;
    bool fPipelineCreationCacheControlEXT = false;
    bool fDriverPropertiesKHR = false;
    bool fCreateRenderpass2KHR = false;
    bool fLoadStoreOpNoneEXT = false;
    bool fLoadStoreOpNoneKHR = false;
    bool fConservativeRasterizationEXT = false;
    bool fPipelineLibraryKHR = false;
    bool fCopyCommands2KHR = false;
    bool fFormatFeatureFlags2KHR = false;
    bool fDepthStencilResolveKHR = false;

    // Featureless extensions that the application may have enabled and that Skia may need to enable
    // via a VkPhysicalDeviceVulkanNNFeatures struct. See "Table 1. Extension Feature Aliases" in
    // the Features chapter of the spec:
    //
    //    Extension:                            Features on promotion to core:
    //    VK_KHR_shader_draw_parameters         shaderDrawParameters
    //    VK_KHR_draw_indirect_count            drawIndirectCount
    //    VK_KHR_sampler_mirror_clamp_to_edge   samplerMirrorClampToEdge
    //    VK_EXT_descriptor_indexing            descriptorIndexing
    //    VK_EXT_sampler_filter_minmax          samplerFilterMinmax
    //    VK_EXT_shader_viewport_index_layer    shaderOutputViewportIndex, shaderOutputLayer
    //    VK_KHR_push_descriptor                pushDescriptor
    bool fShaderDrawParametersKHR = false;
    bool fDrawIndirectCountKHR = false;
    bool fSamplerMirrorClampToEdgeKHR = false;
    bool fDescriptorIndexingEXT = false;
    bool fSamplerFilterMinmaxEXT = false;
    bool fShaderViewportIndexLayerEXT = false;
    bool fPushDescriptorKHR = false;
};

void mark_device_extensions(DeviceExtensions& exts, const char* name) {
    if (strcmp(name, VK_ARM_RASTERIZATION_ORDER_ATTACHMENT_ACCESS_EXTENSION_NAME) == 0) {
        exts.fRasterizationOrderAttachmentAccessARM = true;
    } else if (strcmp(name, VK_EXT_RASTERIZATION_ORDER_ATTACHMENT_ACCESS_EXTENSION_NAME) == 0) {
        exts.fRasterizationOrderAttachmentAccessEXT = true;
    } else if (strcmp(name, VK_EXT_BLEND_OPERATION_ADVANCED_EXTENSION_NAME) == 0) {
        exts.fBlendOperationAdvancedEXT = true;
    } else if (strcmp(name, VK_EXT_EXTENDED_DYNAMIC_STATE_EXTENSION_NAME) == 0) {
        exts.fExtendedDynamicStateEXT = true;
    } else if (strcmp(name, VK_EXT_EXTENDED_DYNAMIC_STATE_2_EXTENSION_NAME) == 0) {
        exts.fExtendedDynamicState2EXT = true;
    } else if (strcmp(name, VK_EXT_VERTEX_INPUT_DYNAMIC_STATE_EXTENSION_NAME) == 0) {
        exts.fVertexInputDynamicStateEXT = true;
    } else if (strcmp(name, VK_EXT_GRAPHICS_PIPELINE_LIBRARY_EXTENSION_NAME) == 0) {
        exts.fGraphicsPipelineLibraryEXT = true;
    } else if (strcmp(name, VK_KHR_SAMPLER_YCBCR_CONVERSION_EXTENSION_NAME) == 0) {
        exts.fSamplerYcbcrConversionKHR = true;
    } else if (strcmp(name, VK_EXT_RGBA10X6_FORMATS_EXTENSION_NAME) == 0) {
        exts.fRGBA10x6FormatsEXT = true;
    } else if (strcmp(name, VK_KHR_SYNCHRONIZATION_2_EXTENSION_NAME) == 0) {
        exts.fSynchronization2KHR = true;
    } else if (strcmp(name, VK_KHR_DYNAMIC_RENDERING_EXTENSION_NAME) == 0) {
        exts.fDynamicRenderingKHR = true;
    } else if (strcmp(name, VK_KHR_DYNAMIC_RENDERING_LOCAL_READ_EXTENSION_NAME) == 0) {
        exts.fDynamicRenderingLocalReadKHR = true;
    } else if (strcmp(name, VK_EXT_MULTISAMPLED_RENDER_TO_SINGLE_SAMPLED_EXTENSION_NAME) == 0) {
        exts.fMultisampledRenderToSingleSampledEXT = true;
    } else if (strcmp(name, VK_EXT_HOST_IMAGE_COPY_EXTENSION_NAME) == 0) {
        exts.fHostImageCopyEXT = true;
    } else if (strcmp(name, VK_EXT_PIPELINE_CREATION_CACHE_CONTROL_EXTENSION_NAME) == 0) {
        exts.fPipelineCreationCacheControlEXT = true;
    } else if (strcmp(name, VK_KHR_DRIVER_PROPERTIES_EXTENSION_NAME) == 0) {
        exts.fDriverPropertiesKHR = true;
    } else if (strcmp(name, VK_KHR_CREATE_RENDERPASS_2_EXTENSION_NAME) == 0) {
        exts.fCreateRenderpass2KHR = true;
    } else if (strcmp(name, VK_EXT_LOAD_STORE_OP_NONE_EXTENSION_NAME) == 0) {
        exts.fLoadStoreOpNoneEXT = true;
    } else if (strcmp(name, VK_KHR_LOAD_STORE_OP_NONE_EXTENSION_NAME) == 0) {
        exts.fLoadStoreOpNoneKHR = true;
    } else if (strcmp(name, VK_EXT_CONSERVATIVE_RASTERIZATION_EXTENSION_NAME) == 0) {
        exts.fConservativeRasterizationEXT = true;
    } else if (strcmp(name, VK_KHR_PIPELINE_LIBRARY_EXTENSION_NAME) == 0) {
        exts.fPipelineLibraryKHR = true;
    } else if (strcmp(name, VK_KHR_COPY_COMMANDS_2_EXTENSION_NAME) == 0) {
        exts.fCopyCommands2KHR = true;
    } else if (strcmp(name, VK_KHR_FORMAT_FEATURE_FLAGS_2_EXTENSION_NAME) == 0) {
        exts.fFormatFeatureFlags2KHR = true;
    } else if (strcmp(name, VK_KHR_DEPTH_STENCIL_RESOLVE_EXTENSION_NAME) == 0) {
        exts.fDepthStencilResolveKHR = true;
    } else if (strcmp(name, VK_KHR_SHADER_DRAW_PARAMETERS_EXTENSION_NAME) == 0) {
        exts.fShaderDrawParametersKHR = true;
    } else if (strcmp(name, VK_KHR_DRAW_INDIRECT_COUNT_EXTENSION_NAME) == 0) {
        exts.fDrawIndirectCountKHR = true;
    } else if (strcmp(name, VK_KHR_SAMPLER_MIRROR_CLAMP_TO_EDGE_EXTENSION_NAME) == 0) {
        exts.fSamplerMirrorClampToEdgeKHR = true;
    } else if (strcmp(name, VK_EXT_DESCRIPTOR_INDEXING_EXTENSION_NAME) == 0) {
        exts.fDescriptorIndexingEXT = true;
    } else if (strcmp(name, VK_EXT_SAMPLER_FILTER_MINMAX_EXTENSION_NAME) == 0) {
        exts.fSamplerFilterMinmaxEXT = true;
    } else if (strcmp(name, VK_EXT_SHADER_VIEWPORT_INDEX_LAYER_EXTENSION_NAME) == 0) {
        exts.fShaderViewportIndexLayerEXT = true;
    } else if (strcmp(name, VK_KHR_PUSH_DESCRIPTOR_EXTENSION_NAME) == 0) {
        exts.fPushDescriptorKHR = true;
    }
}

DeviceExtensions get_supported_device_extensions(const VkExtensionProperties* deviceExtensions,
                                                 size_t deviceExtensionCount) {
    DeviceExtensions exts;

    for (size_t i = 0; i < deviceExtensionCount; ++i) {
        mark_device_extensions(exts, deviceExtensions[i].extensionName);
    }

    return exts;
}

struct FeaturesToAdd {
    bool fVulkan11 = true;
    bool fVulkan12 = true;
    bool fVulkan13 = true;
    bool fVulkan14 = true;
    bool fRasterizationOrderAttachmentAccess = true;
    bool fBlendOperationAdvanced = true;
    bool fExtendedDynamicState = true;
    bool fExtendedDynamicState2 = true;
    bool fVertexInputDynamicState = true;
    bool fGraphicsPipelineLibrary = true;
    bool fSamplerYcbcrConversion = true;
    bool fRGBA10x6Formats = true;
    bool fSynchronization2 = true;
    bool fDynamicRendering = true;
    bool fDynamicRenderingLocalRead = true;
    bool fMultisampledRenderToSingleSampled = true;
    bool fHostImageCopy = true;
    bool fPipelineCreationCacheControl = true;
};

FeaturesToAdd get_features_to_add(uint32_t apiVersion,
                                  const DeviceExtensions& exts,
                                  const VkPhysicalDeviceFeatures2& appFeatures) {
    FeaturesToAdd toAdd;

    // First, exclude feature structs that are not available / are unnecessary based on apiVersion
    // and available extensions.
    if (apiVersion >= VK_API_VERSION_1_4) {
        // VK_KHR_dynamic_rendering_local_read's feature is included in
        // VkPhysicalDeviceVulkan14Features
        toAdd.fDynamicRenderingLocalRead = false;
        // VK_EXT_host_image_copy's feature is included in VkPhysicalDeviceVulkan14Features
        toAdd.fHostImageCopy = false;
    } else {
        toAdd.fVulkan14 = false;
        // Check for support via individual extensions' feature structs
        toAdd.fDynamicRenderingLocalRead = exts.fDynamicRenderingLocalReadKHR;
        toAdd.fHostImageCopy = exts.fHostImageCopyEXT;
    }
    if (apiVersion >= VK_API_VERSION_1_3) {
        // VK_KHR_synchronization2's feature is included in VkPhysicalDeviceVulkan13Features
        toAdd.fSynchronization2 = false;
        // VK_KHR_dynamic_rendering's feature is included in VkPhysicalDeviceVulkan13Features
        toAdd.fDynamicRendering = false;
        // VK_EXT_pipeline_creation_cache_control's feature is included in
        // VkPhysicalDeviceVulkan13Features
        toAdd.fPipelineCreationCacheControl = false;
        // VK_EXT_extended_dynamic_state is core in Vulkan 1.3, but no feature for it is present in
        // VkPhysicalDeviceVulkan13Features; it's assumed supported / enabled.
        toAdd.fExtendedDynamicState = false;
        // Note: VK_EXT_extended_dynamic_state2 was partially promoted to Vulkan 1.3. Only dynamic
        // state from the main feature is used by Skia, so it's ok to rely on Vulkan 1.3 to provide
        // them, but if the optional features of this extension are needed, the extension struct
        // should still be used to query and enable those features.
        toAdd.fExtendedDynamicState2 = false;
    } else {
        toAdd.fVulkan13 = false;
        // Check for support via individual extensions' feature structs
        toAdd.fSynchronization2 = exts.fSynchronization2KHR;
        toAdd.fDynamicRendering = exts.fDynamicRenderingKHR;
        toAdd.fPipelineCreationCacheControl = exts.fPipelineCreationCacheControlEXT;
        toAdd.fExtendedDynamicState = exts.fExtendedDynamicStateEXT;
        toAdd.fExtendedDynamicState2 = exts.fExtendedDynamicState2EXT;
    }
    if (apiVersion >= VK_API_VERSION_1_2) {
        // VK_KHR_sampler_ycbcr_conversion's feature is included in
        // VkPhysicalDeviceVulkan11Features. Note that this struct was introduced in Vulkan 1.2.
        toAdd.fSamplerYcbcrConversion = false;
        // No feature from VkPhysicalDeviceVulkan12Features is used by Skia currently.
    } else {
        toAdd.fVulkan12 = false;
        toAdd.fVulkan11 = false;
        // Check for support via individual extensions' feature structs
        toAdd.fSamplerYcbcrConversion = exts.fSamplerYcbcrConversionKHR;
    }

    // Check for support via individual extensions' feature structs (non of which have been promoted
    // to core).
    toAdd.fRasterizationOrderAttachmentAccess = exts.fRasterizationOrderAttachmentAccessARM ||
                                                exts.fRasterizationOrderAttachmentAccessEXT;
    toAdd.fBlendOperationAdvanced = exts.fBlendOperationAdvancedEXT;
    toAdd.fVertexInputDynamicState = exts.fVertexInputDynamicStateEXT;
    toAdd.fGraphicsPipelineLibrary = exts.fGraphicsPipelineLibraryEXT;
    toAdd.fRGBA10x6Formats = exts.fRGBA10x6FormatsEXT;
    toAdd.fMultisampledRenderToSingleSampled = exts.fMultisampledRenderToSingleSampledEXT;

    // Then, go over appFeatures::pNext and exclude features that are already being queried by the
    // app.
    const VkBaseInStructure* pNext = static_cast<const VkBaseInStructure*>(appFeatures.pNext);
    while (pNext) {
        switch (pNext->sType) {
            case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_1_FEATURES:
                toAdd.fVulkan11 = false;
                break;
            case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_2_FEATURES:
                toAdd.fVulkan12 = false;
                break;
            case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_3_FEATURES:
                toAdd.fVulkan13 = false;
                break;
            case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_4_FEATURES:
                toAdd.fVulkan14 = false;
                break;
            case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_RASTERIZATION_ORDER_ATTACHMENT_ACCESS_FEATURES_EXT:
                toAdd.fRasterizationOrderAttachmentAccess = false;
                break;
            case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_BLEND_OPERATION_ADVANCED_FEATURES_EXT:
                toAdd.fBlendOperationAdvanced = false;
                break;
            case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_EXTENDED_DYNAMIC_STATE_FEATURES_EXT:
                toAdd.fExtendedDynamicState = false;
                break;
            case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_EXTENDED_DYNAMIC_STATE_2_FEATURES_EXT:
                toAdd.fExtendedDynamicState2 = false;
                break;
            case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VERTEX_INPUT_DYNAMIC_STATE_FEATURES_EXT:
                toAdd.fVertexInputDynamicState = false;
                break;
            case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_GRAPHICS_PIPELINE_LIBRARY_FEATURES_EXT:
                toAdd.fGraphicsPipelineLibrary = false;
                break;
            case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SAMPLER_YCBCR_CONVERSION_FEATURES:
                toAdd.fSamplerYcbcrConversion = false;
                break;
            case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_RGBA10X6_FORMATS_FEATURES_EXT:
                toAdd.fRGBA10x6Formats = false;
                break;
            case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SYNCHRONIZATION_2_FEATURES:
                toAdd.fSynchronization2 = false;
                break;
            case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DYNAMIC_RENDERING_FEATURES:
                toAdd.fDynamicRendering = false;
                break;
            case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DYNAMIC_RENDERING_LOCAL_READ_FEATURES:
                toAdd.fDynamicRenderingLocalRead = false;
                break;
            case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MULTISAMPLED_RENDER_TO_SINGLE_SAMPLED_FEATURES_EXT:
                toAdd.fMultisampledRenderToSingleSampled = false;
                break;
            case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_HOST_IMAGE_COPY_FEATURES:
                toAdd.fHostImageCopy = false;
                break;
            case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PIPELINE_CREATION_CACHE_CONTROL_FEATURES:
                toAdd.fPipelineCreationCacheControl = false;
                break;
            default:
                break;
        }

        pNext = pNext->pNext;
    }

    return toAdd;
}

template <typename VulkanStruct> void chain(void**& chainEnd, VulkanStruct* toAdd) {
    *chainEnd = toAdd;
    chainEnd = (void**)&toAdd->pNext;
}
}  // anonymous namespace

VulkanPreferredFeatures::~VulkanPreferredFeatures() {
    if (fHasAddedToInstanceExtensions && !fHasAddedFeaturesToQuery) {
        SkDebugf("WARNING: VulkanPreferredFeatures::addFeaturesToQuery() was not called\n");
    }
    if (fHasAddedToInstanceExtensions && !fHasAddedFeaturesToEnable) {
        SkDebugf("WARNING: VulkanPreferredFeatures::addFeaturesToEnable() was not called\n");
    }
}

void VulkanPreferredFeatures::init(uint32_t appAPIVersion) {
    fAPIVersion = appAPIVersion;

    // Minimum required version is Vulkan 1.1
    SkASSERT(fAPIVersion >= VK_API_VERSION_1_1);
    // This class does not understand Vulkan versions higher than 1.4
    SkASSERT(fAPIVersion <= VK_API_VERSION_1_4);
}

void VulkanPreferredFeatures::addToInstanceExtensions(
        const VkExtensionProperties* instanceExtensions,
        size_t instanceExtensionCount,
        std::vector<const char*>& appExtensions) {
    // Nothing to do, currently Skia does not take advantage of any additional instance extensions.
    fHasAddedToInstanceExtensions = true;
}

void VulkanPreferredFeatures::addFeaturesToQuery(const VkExtensionProperties* deviceExtensions,
                                                 size_t deviceExtensionCount,
                                                 VkPhysicalDeviceFeatures2& appFeatures) {
    // First, inspect the list of device extensions and note which are available.
    DeviceExtensions exts =
            get_supported_device_extensions(deviceExtensions, deviceExtensionCount);

    // For now, pretend these extensions are not supported. This de-risks using this API by users
    // who didn't previously enable them. Those already in use by Skia will be re-enabled shortly
    // after. Those not yet in use by Skia will be enabled in the future when they start to get
    // used.
#if defined(SK_DISABLE_GRAPHICS_PIPELINE_LIBRARY)
    exts.fPipelineLibraryKHR = false;
    exts.fGraphicsPipelineLibraryEXT = false;
#endif
    exts.fSynchronization2KHR = false;
    exts.fDynamicRenderingKHR = false;
    exts.fDynamicRenderingLocalReadKHR = false;
#if defined(SK_DISABLE_HOST_IMAGE_COPY)
    exts.fHostImageCopyEXT = false;
    exts.fCopyCommands2KHR = false;
#endif
    exts.fShaderDrawParametersKHR = false;
    exts.fDrawIndirectCountKHR = false;
    exts.fSamplerMirrorClampToEdgeKHR = false;
    exts.fDescriptorIndexingEXT = false;
    exts.fSamplerFilterMinmaxEXT = false;
    exts.fShaderViewportIndexLayerEXT = false;
    exts.fPushDescriptorKHR = false;

    // Set the sType and extensions to enable. Later in addFeaturesToEnable, the availability of
    // device extensions is inferred from this. This is done irrespective of FeaturesToAdd
    // calculated below, because if an app includes a feature struct that has a desirable feature
    // disabled we can enable it later in addFeaturesToEnable. This is only possible if we know the
    // feature is supported (inferred from the sType being set here) and the feature is required by
    // the spec to be supported when the extension is available.
    if (fAPIVersion >= VK_API_VERSION_1_4) {
        fVulkan14.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_4_FEATURES;
    } else {
        if (exts.fDynamicRenderingLocalReadKHR) {
            fDynamicRenderingLocalRead.sType =
                    VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DYNAMIC_RENDERING_LOCAL_READ_FEATURES;
        }
        if (exts.fHostImageCopyEXT) {
            fHostImageCopy.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_HOST_IMAGE_COPY_FEATURES;
        }
        if (exts.fLoadStoreOpNoneKHR) {
            fLoadStoreOpNoneExtension = VK_KHR_LOAD_STORE_OP_NONE_EXTENSION_NAME;
        } else if (exts.fLoadStoreOpNoneEXT) {
            fLoadStoreOpNoneExtension = VK_EXT_LOAD_STORE_OP_NONE_EXTENSION_NAME;
        }
    }
    if (fAPIVersion >= VK_API_VERSION_1_3) {
        fVulkan13.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_3_FEATURES;
    } else {
        if (exts.fSynchronization2KHR) {
            fSynchronization2.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SYNCHRONIZATION_2_FEATURES;
        }
        if (exts.fDynamicRenderingKHR) {
            fDynamicRendering.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DYNAMIC_RENDERING_FEATURES;
        }
        if (exts.fPipelineCreationCacheControlEXT) {
            fPipelineCreationCacheControl.sType =
                    VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PIPELINE_CREATION_CACHE_CONTROL_FEATURES;
        }
        if (exts.fExtendedDynamicStateEXT) {
            fExtendedDynamicState.sType =
                    VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_EXTENDED_DYNAMIC_STATE_FEATURES_EXT;
        }
        if (exts.fExtendedDynamicState2EXT) {
            fExtendedDynamicState2.sType =
                    VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_EXTENDED_DYNAMIC_STATE_2_FEATURES_EXT;
        }
        if (exts.fCopyCommands2KHR) {
            fCopyCommands2Extension = VK_KHR_COPY_COMMANDS_2_EXTENSION_NAME;
        }
        if (exts.fFormatFeatureFlags2KHR) {
            fFormatFeatureFlags2Extension = VK_KHR_FORMAT_FEATURE_FLAGS_2_EXTENSION_NAME;
        }
    }
    if (fAPIVersion >= VK_API_VERSION_1_2) {
        fVulkan11.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_1_FEATURES;
        fVulkan12.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_2_FEATURES;
    } else {
        if (exts.fSamplerYcbcrConversionKHR) {
            fSamplerYcbcrConversion.sType =
                    VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SAMPLER_YCBCR_CONVERSION_FEATURES;
        }
        if (exts.fDriverPropertiesKHR) {
            fDriverPropertiesExtension = VK_KHR_DRIVER_PROPERTIES_EXTENSION_NAME;
        }
        if (exts.fCreateRenderpass2KHR) {
            fCreateRenderpass2Extension = VK_KHR_CREATE_RENDERPASS_2_EXTENSION_NAME;
        }
        if (exts.fDepthStencilResolveKHR) {
            fDepthStencilResolveExtension = VK_KHR_DEPTH_STENCIL_RESOLVE_EXTENSION_NAME;
        }
    }
    SkASSERT(fAPIVersion >= VK_API_VERSION_1_1);

    if (exts.fRasterizationOrderAttachmentAccessARM) {
        fRasterizationOrderAttachmentAccess.sType =
                VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_RASTERIZATION_ORDER_ATTACHMENT_ACCESS_FEATURES_ARM;
        fRasterizationOrderAttachmentAccessExtension =
                VK_ARM_RASTERIZATION_ORDER_ATTACHMENT_ACCESS_EXTENSION_NAME;
    }
    if (exts.fRasterizationOrderAttachmentAccessEXT) {
        fRasterizationOrderAttachmentAccess.sType =
                VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_RASTERIZATION_ORDER_ATTACHMENT_ACCESS_FEATURES_EXT;
        fRasterizationOrderAttachmentAccessExtension =
                VK_EXT_RASTERIZATION_ORDER_ATTACHMENT_ACCESS_EXTENSION_NAME;
    }
    if (exts.fBlendOperationAdvancedEXT) {
        fBlendOperationAdvanced.sType =
                VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_BLEND_OPERATION_ADVANCED_FEATURES_EXT;
    }
    if (exts.fVertexInputDynamicStateEXT) {
        fVertexInputDynamicState.sType =
                VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VERTEX_INPUT_DYNAMIC_STATE_FEATURES_EXT;
    }
    if (exts.fGraphicsPipelineLibraryEXT) {
        fGraphicsPipelineLibrary.sType =
                VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_GRAPHICS_PIPELINE_LIBRARY_FEATURES_EXT;
        // Depends on VK_KHR_pipeline_library
        fPipelineLibraryExtension = VK_KHR_PIPELINE_LIBRARY_EXTENSION_NAME;
    }
    if (exts.fRGBA10x6FormatsEXT) {
        fRGBA10x6Formats.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_RGBA10X6_FORMATS_FEATURES_EXT;
    }
    if (exts.fMultisampledRenderToSingleSampledEXT) {
        fMultisampledRenderToSingleSampled.sType =
                VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MULTISAMPLED_RENDER_TO_SINGLE_SAMPLED_FEATURES_EXT;
    }
    if (exts.fConservativeRasterizationEXT) {
        fConservativeRasterizationExtension = VK_EXT_CONSERVATIVE_RASTERIZATION_EXTENSION_NAME;
    }

    // Inspect the list of features the app has already included and decide on which features to
    // add.
    const FeaturesToAdd toAdd = get_features_to_add(fAPIVersion, exts, appFeatures);

    // Chain any new features we'd like to query.
    if (toAdd.fVulkan11) {
        SkASSERT(fVulkan11.sType != 0);
        AddToPNextChain(&appFeatures, &fVulkan11);
    }
    if (toAdd.fVulkan12) {
        SkASSERT(fVulkan12.sType != 0);
        AddToPNextChain(&appFeatures, &fVulkan12);
    }
    if (toAdd.fVulkan13) {
        SkASSERT(fVulkan13.sType != 0);
        AddToPNextChain(&appFeatures, &fVulkan13);
    }
    if (toAdd.fVulkan14) {
        SkASSERT(fVulkan14.sType != 0);
        AddToPNextChain(&appFeatures, &fVulkan14);
    }
    if (toAdd.fRasterizationOrderAttachmentAccess) {
        SkASSERT(fRasterizationOrderAttachmentAccess.sType != 0);
        AddToPNextChain(&appFeatures, &fRasterizationOrderAttachmentAccess);
    }
    if (toAdd.fBlendOperationAdvanced) {
        SkASSERT(fBlendOperationAdvanced.sType != 0);
        AddToPNextChain(&appFeatures, &fBlendOperationAdvanced);
    }
    if (toAdd.fExtendedDynamicState) {
        SkASSERT(fExtendedDynamicState.sType != 0);
        AddToPNextChain(&appFeatures, &fExtendedDynamicState);
    }
    if (toAdd.fExtendedDynamicState2) {
        SkASSERT(fExtendedDynamicState2.sType != 0);
        AddToPNextChain(&appFeatures, &fExtendedDynamicState2);
    }
    if (toAdd.fVertexInputDynamicState) {
        SkASSERT(fVertexInputDynamicState.sType != 0);
        AddToPNextChain(&appFeatures, &fVertexInputDynamicState);
    }
    if (toAdd.fGraphicsPipelineLibrary) {
        SkASSERT(fGraphicsPipelineLibrary.sType != 0);
        AddToPNextChain(&appFeatures, &fGraphicsPipelineLibrary);
    }
    if (toAdd.fSamplerYcbcrConversion) {
        SkASSERT(fSamplerYcbcrConversion.sType != 0);
        AddToPNextChain(&appFeatures, &fSamplerYcbcrConversion);
    }
    if (toAdd.fRGBA10x6Formats) {
        SkASSERT(fRGBA10x6Formats.sType != 0);
        AddToPNextChain(&appFeatures, &fRGBA10x6Formats);
    }
    if (toAdd.fSynchronization2) {
        SkASSERT(fSynchronization2.sType != 0);
        AddToPNextChain(&appFeatures, &fSynchronization2);
    }
    if (toAdd.fDynamicRendering) {
        SkASSERT(fDynamicRendering.sType != 0);
        AddToPNextChain(&appFeatures, &fDynamicRendering);
    }
    if (toAdd.fDynamicRenderingLocalRead) {
        SkASSERT(fDynamicRenderingLocalRead.sType != 0);
        AddToPNextChain(&appFeatures, &fDynamicRenderingLocalRead);
    }
    if (toAdd.fMultisampledRenderToSingleSampled) {
        SkASSERT(fMultisampledRenderToSingleSampled.sType != 0);
        AddToPNextChain(&appFeatures, &fMultisampledRenderToSingleSampled);
    }
    if (toAdd.fHostImageCopy) {
        SkASSERT(fHostImageCopy.sType != 0);
        AddToPNextChain(&appFeatures, &fHostImageCopy);
    }
    if (toAdd.fPipelineCreationCacheControl) {
        SkASSERT(fPipelineCreationCacheControl.sType != 0);
        AddToPNextChain(&appFeatures, &fPipelineCreationCacheControl);
    }

    fHasAddedFeaturesToQuery = true;
}

void VulkanPreferredFeatures::addFeaturesToEnable(std::vector<const char*>& appExtensions,
                                                  VkPhysicalDeviceFeatures2& appFeatures) {
    if (!fHasAddedFeaturesToQuery) {
        SkDebugf(
                "WARNING: VulkanPreferredFeatures::addFeaturesToQuery() was not called before "
                "addFeaturesToEnable, performance may degrade\n");
    }
    SkASSERT(fHasAddedFeaturesToQuery);

    FeaturesToAdd toAdd;
    toAdd.fVulkan11 = fVulkan11.sType != 0;
    toAdd.fVulkan12 = fVulkan12.sType != 0;
    toAdd.fVulkan13 = fVulkan13.sType != 0;
    toAdd.fVulkan14 = fVulkan14.sType != 0;
    toAdd.fRasterizationOrderAttachmentAccess = fRasterizationOrderAttachmentAccess.sType != 0;
    toAdd.fBlendOperationAdvanced = fBlendOperationAdvanced.sType != 0;
    toAdd.fExtendedDynamicState = fExtendedDynamicState.sType != 0;
    toAdd.fExtendedDynamicState2 = fExtendedDynamicState2.sType != 0;
    toAdd.fVertexInputDynamicState = fVertexInputDynamicState.sType != 0;
    toAdd.fGraphicsPipelineLibrary = fGraphicsPipelineLibrary.sType != 0;
    toAdd.fSamplerYcbcrConversion = fSamplerYcbcrConversion.sType != 0;
    toAdd.fRGBA10x6Formats = fRGBA10x6Formats.sType != 0;
    toAdd.fSynchronization2 = fSynchronization2.sType != 0;
    toAdd.fDynamicRendering = fDynamicRendering.sType != 0;
    toAdd.fDynamicRenderingLocalRead = fDynamicRenderingLocalRead.sType != 0;
    toAdd.fMultisampledRenderToSingleSampled = fMultisampledRenderToSingleSampled.sType != 0;
    toAdd.fHostImageCopy = fHostImageCopy.sType != 0;
    toAdd.fPipelineCreationCacheControl = fPipelineCreationCacheControl.sType != 0;

    // Check which extensions are already enabled by the application.
    DeviceExtensions exts;
    for (const char* name : appExtensions) {
        mark_device_extensions(exts, name);
    }

    // Add extensions that are not enabled by the app.
    if (!exts.fRasterizationOrderAttachmentAccessARM &&
        !exts.fRasterizationOrderAttachmentAccessEXT && toAdd.fRasterizationOrderAttachmentAccess) {
        appExtensions.push_back(fRasterizationOrderAttachmentAccessExtension);
        exts.fRasterizationOrderAttachmentAccessEXT = true;
    }
    if (!exts.fBlendOperationAdvancedEXT && toAdd.fBlendOperationAdvanced) {
        appExtensions.push_back(VK_EXT_BLEND_OPERATION_ADVANCED_EXTENSION_NAME);
        exts.fBlendOperationAdvancedEXT = true;
    }
    if (!exts.fExtendedDynamicStateEXT && toAdd.fExtendedDynamicState) {
        appExtensions.push_back(VK_EXT_EXTENDED_DYNAMIC_STATE_EXTENSION_NAME);
        exts.fExtendedDynamicStateEXT = true;
    }
    if (!exts.fExtendedDynamicState2EXT && toAdd.fExtendedDynamicState2) {
        appExtensions.push_back(VK_EXT_EXTENDED_DYNAMIC_STATE_2_EXTENSION_NAME);
        exts.fExtendedDynamicState2EXT = true;
    }
    if (!exts.fVertexInputDynamicStateEXT && toAdd.fVertexInputDynamicState) {
        appExtensions.push_back(VK_EXT_VERTEX_INPUT_DYNAMIC_STATE_EXTENSION_NAME);
        exts.fVertexInputDynamicStateEXT = true;
    }
    if (!exts.fGraphicsPipelineLibraryEXT && toAdd.fGraphicsPipelineLibrary) {
        appExtensions.push_back(VK_EXT_GRAPHICS_PIPELINE_LIBRARY_EXTENSION_NAME);
        exts.fGraphicsPipelineLibraryEXT = true;
    }
    if (!exts.fSamplerYcbcrConversionKHR && toAdd.fSamplerYcbcrConversion) {
        appExtensions.push_back(VK_KHR_SAMPLER_YCBCR_CONVERSION_EXTENSION_NAME);
        exts.fSamplerYcbcrConversionKHR = true;
    }
    if (!exts.fRGBA10x6FormatsEXT && toAdd.fRGBA10x6Formats) {
        appExtensions.push_back(VK_EXT_RGBA10X6_FORMATS_EXTENSION_NAME);
        exts.fRGBA10x6FormatsEXT = true;
    }
    if (!exts.fSynchronization2KHR && toAdd.fSynchronization2) {
        appExtensions.push_back(VK_KHR_SYNCHRONIZATION_2_EXTENSION_NAME);
        exts.fSynchronization2KHR = true;
    }
    if (!exts.fDynamicRenderingKHR && toAdd.fDynamicRendering) {
        appExtensions.push_back(VK_KHR_DYNAMIC_RENDERING_EXTENSION_NAME);
        exts.fDynamicRenderingKHR = true;
    }
    if (!exts.fDynamicRenderingLocalReadKHR && toAdd.fDynamicRenderingLocalRead) {
        appExtensions.push_back(VK_KHR_DYNAMIC_RENDERING_LOCAL_READ_EXTENSION_NAME);
        exts.fDynamicRenderingLocalReadKHR = true;
    }
    if (!exts.fMultisampledRenderToSingleSampledEXT && toAdd.fMultisampledRenderToSingleSampled) {
        appExtensions.push_back(VK_EXT_MULTISAMPLED_RENDER_TO_SINGLE_SAMPLED_EXTENSION_NAME);
        exts.fMultisampledRenderToSingleSampledEXT = true;
    }
    if (!exts.fHostImageCopyEXT && toAdd.fHostImageCopy) {
        appExtensions.push_back(VK_EXT_HOST_IMAGE_COPY_EXTENSION_NAME);
        exts.fHostImageCopyEXT = true;
    }
    if (!exts.fPipelineCreationCacheControlEXT && toAdd.fPipelineCreationCacheControl) {
        appExtensions.push_back(VK_EXT_PIPELINE_CREATION_CACHE_CONTROL_EXTENSION_NAME);
        exts.fPipelineCreationCacheControlEXT = true;
    }
    if (!exts.fDriverPropertiesKHR && fDriverPropertiesExtension != nullptr) {
        appExtensions.push_back(fDriverPropertiesExtension);
        exts.fDriverPropertiesKHR = true;
    }
    if (!exts.fCreateRenderpass2KHR && fCreateRenderpass2Extension != nullptr) {
        appExtensions.push_back(fCreateRenderpass2Extension);
        exts.fCreateRenderpass2KHR = true;
    }
    if (!exts.fLoadStoreOpNoneKHR && !exts.fLoadStoreOpNoneEXT &&
        fLoadStoreOpNoneExtension != nullptr) {
        appExtensions.push_back(fLoadStoreOpNoneExtension);
        exts.fCreateRenderpass2KHR = true;
    }
    if (!exts.fConservativeRasterizationEXT && fConservativeRasterizationExtension != nullptr) {
        appExtensions.push_back(fConservativeRasterizationExtension);
        exts.fConservativeRasterizationEXT = true;
    }
    if (!exts.fPipelineLibraryKHR && fPipelineLibraryExtension != nullptr) {
        appExtensions.push_back(fPipelineLibraryExtension);
        exts.fPipelineLibraryKHR = true;
    }
    if (!exts.fCopyCommands2KHR && fCopyCommands2Extension != nullptr) {
        appExtensions.push_back(fCopyCommands2Extension);
        exts.fCopyCommands2KHR = true;
    }
    if (!exts.fFormatFeatureFlags2KHR && fFormatFeatureFlags2Extension != nullptr) {
        appExtensions.push_back(fFormatFeatureFlags2Extension);
        exts.fFormatFeatureFlags2KHR = true;
    }
    if (!exts.fDepthStencilResolveKHR && fDepthStencilResolveExtension != nullptr) {
        appExtensions.push_back(fDepthStencilResolveExtension);
        exts.fDepthStencilResolveKHR = true;
    }

    // Go over the app features.  There are a couple of possibilities:
    //
    // - A struct is included by the app: In this case, if there is a feature Skia wants that
    //   is required to be supported when the extension is present, that feature is enabled.
    //   If an optional feature is supported but disabled by the app, Skia has no visibility into it
    //   right now, but no such feature is used by Skia currently.
    // - A struct is included byFeaturesToQuery: In this case, if there are features that Skia
    //   doesn't need, they are disabled since the app wasn't relying on those features.
    // - If a Vulkan1X feature struct needs to be chained, then the individual feature structs that
    //   it subsumes are removed from the chain and their features are enabled in the Vulkan1X
    //   struct instead.
    //
    // Note that chaining the Vulkan1X feature struct is preferred where possible, because a 1.X
    // driver is not obliged to also expose the extensions that are promoted to core in that
    // version. As such, features are aggregated in Vulkan1X structs when possible and the
    // individual extension-specific structs are dropped. If Skia requires that Vulkan1X structs be
    // used when possible, the feature aggregation code below becomes unnecessary.
    //
    // In the following, if an extension of interest is enabled, its spec-required main feature is
    // enabled. Normally this is not needed, as the feature query would return VK_TRUE for that
    // feature. However, this is done to protect against the app having turned the feature off after
    // query.

    // Create a new chain of structs. AddToPNextChain is not used because the structs being chained
    // may already be present in appFeatures' pNext chain.
    void* newChainStart = nullptr;
    void** newChainEnd = &newChainStart;

    if (toAdd.fVulkan11) {
        // Disable all features not used by Skia
        fVulkan11.storageBuffer16BitAccess = VK_FALSE;
        fVulkan11.uniformAndStorageBuffer16BitAccess = VK_FALSE;
        fVulkan11.storagePushConstant16 = VK_FALSE;
        fVulkan11.storageInputOutput16 = VK_FALSE;
        fVulkan11.multiview = VK_FALSE;
        fVulkan11.multiviewGeometryShader = VK_FALSE;
        fVulkan11.multiviewTessellationShader = VK_FALSE;
        fVulkan11.variablePointersStorageBuffer = VK_FALSE;
        fVulkan11.variablePointers = VK_FALSE;
        fVulkan11.protectedMemory = VK_FALSE;
        fVulkan11.shaderDrawParameters = VK_FALSE;

        // samplerYcbcrConversion is required since Version 1.4.
        if (fAPIVersion >= VK_API_VERSION_1_4) {
            fVulkan11.samplerYcbcrConversion = VK_TRUE;
        }

        // The following are features from promoted extensions, where the original extension did not
        // have a feature.
        if (exts.fShaderDrawParametersKHR) {
            fVulkan11.shaderDrawParameters = VK_TRUE;
        }
    }
    if (toAdd.fVulkan12) {
        // Disable all features not used by Skia
        fVulkan12.samplerMirrorClampToEdge = VK_FALSE;
        fVulkan12.drawIndirectCount = VK_FALSE;
        fVulkan12.storageBuffer8BitAccess = VK_FALSE;
        fVulkan12.uniformAndStorageBuffer8BitAccess = VK_FALSE;
        fVulkan12.storagePushConstant8 = VK_FALSE;
        fVulkan12.shaderBufferInt64Atomics = VK_FALSE;
        fVulkan12.shaderSharedInt64Atomics = VK_FALSE;
        fVulkan12.shaderFloat16 = VK_FALSE;
        fVulkan12.shaderInt8 = VK_FALSE;
        fVulkan12.descriptorIndexing = VK_FALSE;
        fVulkan12.shaderInputAttachmentArrayDynamicIndexing = VK_FALSE;
        fVulkan12.shaderUniformTexelBufferArrayDynamicIndexing = VK_FALSE;
        fVulkan12.shaderStorageTexelBufferArrayDynamicIndexing = VK_FALSE;
        fVulkan12.shaderUniformBufferArrayNonUniformIndexing = VK_FALSE;
        fVulkan12.shaderSampledImageArrayNonUniformIndexing = VK_FALSE;
        fVulkan12.shaderStorageBufferArrayNonUniformIndexing = VK_FALSE;
        fVulkan12.shaderStorageImageArrayNonUniformIndexing = VK_FALSE;
        fVulkan12.shaderInputAttachmentArrayNonUniformIndexing = VK_FALSE;
        fVulkan12.shaderUniformTexelBufferArrayNonUniformIndexing = VK_FALSE;
        fVulkan12.shaderStorageTexelBufferArrayNonUniformIndexing = VK_FALSE;
        fVulkan12.descriptorBindingUniformBufferUpdateAfterBind = VK_FALSE;
        fVulkan12.descriptorBindingSampledImageUpdateAfterBind = VK_FALSE;
        fVulkan12.descriptorBindingStorageImageUpdateAfterBind = VK_FALSE;
        fVulkan12.descriptorBindingStorageBufferUpdateAfterBind = VK_FALSE;
        fVulkan12.descriptorBindingUniformTexelBufferUpdateAfterBind = VK_FALSE;
        fVulkan12.descriptorBindingStorageTexelBufferUpdateAfterBind = VK_FALSE;
        fVulkan12.descriptorBindingUpdateUnusedWhilePending = VK_FALSE;
        fVulkan12.descriptorBindingPartiallyBound = VK_FALSE;
        fVulkan12.descriptorBindingVariableDescriptorCount = VK_FALSE;
        fVulkan12.runtimeDescriptorArray = VK_FALSE;
        fVulkan12.samplerFilterMinmax = VK_FALSE;
        fVulkan12.scalarBlockLayout = VK_FALSE;
        fVulkan12.imagelessFramebuffer = VK_FALSE;
        fVulkan12.uniformBufferStandardLayout = VK_FALSE;
        fVulkan12.shaderSubgroupExtendedTypes = VK_FALSE;
        fVulkan12.separateDepthStencilLayouts = VK_FALSE;
        fVulkan12.hostQueryReset = VK_FALSE;
        fVulkan12.timelineSemaphore = VK_FALSE;
        fVulkan12.bufferDeviceAddress = VK_FALSE;
        fVulkan12.bufferDeviceAddressCaptureReplay = VK_FALSE;
        fVulkan12.bufferDeviceAddressMultiDevice = VK_FALSE;
        fVulkan12.vulkanMemoryModel = VK_FALSE;
        fVulkan12.vulkanMemoryModelDeviceScope = VK_FALSE;
        fVulkan12.vulkanMemoryModelAvailabilityVisibilityChains = VK_FALSE;
        fVulkan12.shaderOutputViewportIndex = VK_FALSE;
        fVulkan12.shaderOutputLayer = VK_FALSE;
        fVulkan12.subgroupBroadcastDynamicId = VK_FALSE;

        // No required feature of Vulkan 1.2 is currently needed. The imagelessFramebuffer feature
        // is a candidate, but likely better to not bother and go directly with dynamic rendering.

        // The following are features from promoted extensions, where the original extension did not
        // have a feature.
        if (exts.fDrawIndirectCountKHR) {
            fVulkan12.drawIndirectCount = VK_TRUE;
        }
        if (exts.fSamplerMirrorClampToEdgeKHR) {
            fVulkan12.samplerMirrorClampToEdge = VK_TRUE;
        }
        if (exts.fDescriptorIndexingEXT) {
            fVulkan12.descriptorIndexing = VK_TRUE;
        }
        if (exts.fSamplerFilterMinmaxEXT) {
            fVulkan12.samplerFilterMinmax = VK_TRUE;
        }
        if (exts.fShaderViewportIndexLayerEXT) {
            fVulkan12.shaderOutputViewportIndex = VK_TRUE;
            fVulkan12.shaderOutputLayer = VK_TRUE;
        }
    }
    if (toAdd.fVulkan13) {
        // Disable all features not used by Skia
        fVulkan13.robustImageAccess = VK_FALSE;
        fVulkan13.inlineUniformBlock = VK_FALSE;
        fVulkan13.descriptorBindingInlineUniformBlockUpdateAfterBind = VK_FALSE;
        fVulkan13.privateData = VK_FALSE;
        fVulkan13.shaderDemoteToHelperInvocation = VK_FALSE;
        fVulkan13.shaderTerminateInvocation = VK_FALSE;
        fVulkan13.subgroupSizeControl = VK_FALSE;
        fVulkan13.computeFullSubgroups = VK_FALSE;
        fVulkan13.textureCompressionASTC_HDR = VK_FALSE;
        fVulkan13.shaderZeroInitializeWorkgroupMemory = VK_FALSE;
        fVulkan13.shaderIntegerDotProduct = VK_FALSE;
        fVulkan13.maintenance4 = VK_FALSE;

        // synchronization2, dynamicRendering and pipelineCreationCacheControl are required features
        // of Vulkan 1.3.
        fVulkan13.synchronization2 = VK_TRUE;
        fVulkan13.dynamicRendering = VK_TRUE;
        fVulkan13.pipelineCreationCacheControl = VK_TRUE;
    }
    if (toAdd.fVulkan14) {
        // Disable all features not used by Skia
        fVulkan14.globalPriorityQuery = VK_FALSE;
        fVulkan14.shaderSubgroupRotate = VK_FALSE;
        fVulkan14.shaderSubgroupRotateClustered = VK_FALSE;
        fVulkan14.shaderFloatControls2 = VK_FALSE;
        fVulkan14.shaderExpectAssume = VK_FALSE;
        fVulkan14.rectangularLines = VK_FALSE;
        fVulkan14.bresenhamLines = VK_FALSE;
        fVulkan14.smoothLines = VK_FALSE;
        fVulkan14.stippledRectangularLines = VK_FALSE;
        fVulkan14.stippledBresenhamLines = VK_FALSE;
        fVulkan14.stippledSmoothLines = VK_FALSE;
        fVulkan14.vertexAttributeInstanceRateDivisor = VK_FALSE;
        fVulkan14.vertexAttributeInstanceRateZeroDivisor = VK_FALSE;
        fVulkan14.indexTypeUint8 = VK_FALSE;
        fVulkan14.maintenance5 = VK_FALSE;
        fVulkan14.maintenance6 = VK_FALSE;
        fVulkan14.pipelineProtectedAccess = VK_FALSE;
        fVulkan14.pipelineRobustness = VK_FALSE;
        fVulkan14.pushDescriptor = VK_FALSE;

        // dynamicRenderingLocalRead is a required feature of Vulkan 1.4.
        fVulkan14.dynamicRenderingLocalRead = VK_TRUE;

        // The following are features from promoted extensions, where the original extension did not
        // have a feature.
        if (exts.fPushDescriptorKHR) {
            fVulkan14.pushDescriptor = VK_TRUE;
        }
    }

    bool hasVulkan11Features = toAdd.fVulkan11;
    bool hasVulkan12Features = toAdd.fVulkan12;
    bool hasVulkan13Features = toAdd.fVulkan13;
    bool hasVulkan14Features = toAdd.fVulkan14;

    // If chained by Skia, disable depth/stencil coherence even if supported, in case it comes with
    // a perf cost.
    fRasterizationOrderAttachmentAccess.rasterizationOrderDepthAttachmentAccess = VK_FALSE;
    fRasterizationOrderAttachmentAccess.rasterizationOrderStencilAttachmentAccess = VK_FALSE;

    // If chained by Skia, disable dynamic state that is unused by Skia.
    fExtendedDynamicState2.extendedDynamicState2LogicOp = VK_FALSE;
    fExtendedDynamicState2.extendedDynamicState2PatchControlPoints = VK_FALSE;

// Helper to enable a feature in the feature aggregate struct if it's enabled in the
// extension-specific struct.  It reduces code verbosity but more importantly ensures the feature
// name is identical and reduces chance of copy-paste mistakes.
#define DUP_INTO(aggregate, feature) aggregate.feature |= features->feature

    VkBaseOutStructure* pNext = static_cast<VkBaseOutStructure*>(appFeatures.pNext);
    while (pNext) {
        switch (pNext->sType) {
            case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_1_FEATURES: {
                auto* features = reinterpret_cast<VkPhysicalDeviceVulkan11Features*>(pNext);
                // If this struct is not chained by Skia, copy enabled features.
                if (features != &fVulkan11) {
                    DUP_INTO(fVulkan11, storageBuffer16BitAccess);
                    DUP_INTO(fVulkan11, uniformAndStorageBuffer16BitAccess);
                    DUP_INTO(fVulkan11, storagePushConstant16);
                    DUP_INTO(fVulkan11, storageInputOutput16);
                    DUP_INTO(fVulkan11, multiview);
                    DUP_INTO(fVulkan11, multiviewGeometryShader);
                    DUP_INTO(fVulkan11, multiviewTessellationShader);
                    DUP_INTO(fVulkan11, variablePointersStorageBuffer);
                    DUP_INTO(fVulkan11, variablePointers);
                    DUP_INTO(fVulkan11, protectedMemory);
                    DUP_INTO(fVulkan11, samplerYcbcrConversion);
                    DUP_INTO(fVulkan11, shaderDrawParameters);
                }
                break;
            }
            case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_2_FEATURES: {
                auto* features = reinterpret_cast<VkPhysicalDeviceVulkan12Features*>(pNext);
                // If this struct is not chained by Skia, copy enabled features.
                if (features != &fVulkan12) {
                    DUP_INTO(fVulkan12, samplerMirrorClampToEdge);
                    DUP_INTO(fVulkan12, drawIndirectCount);
                    DUP_INTO(fVulkan12, storageBuffer8BitAccess);
                    DUP_INTO(fVulkan12, uniformAndStorageBuffer8BitAccess);
                    DUP_INTO(fVulkan12, storagePushConstant8);
                    DUP_INTO(fVulkan12, shaderBufferInt64Atomics);
                    DUP_INTO(fVulkan12, shaderSharedInt64Atomics);
                    DUP_INTO(fVulkan12, shaderFloat16);
                    DUP_INTO(fVulkan12, shaderInt8);
                    DUP_INTO(fVulkan12, descriptorIndexing);
                    DUP_INTO(fVulkan12, shaderInputAttachmentArrayDynamicIndexing);
                    DUP_INTO(fVulkan12, shaderUniformTexelBufferArrayDynamicIndexing);
                    DUP_INTO(fVulkan12, shaderStorageTexelBufferArrayDynamicIndexing);
                    DUP_INTO(fVulkan12, shaderUniformBufferArrayNonUniformIndexing);
                    DUP_INTO(fVulkan12, shaderSampledImageArrayNonUniformIndexing);
                    DUP_INTO(fVulkan12, shaderStorageBufferArrayNonUniformIndexing);
                    DUP_INTO(fVulkan12, shaderStorageImageArrayNonUniformIndexing);
                    DUP_INTO(fVulkan12, shaderInputAttachmentArrayNonUniformIndexing);
                    DUP_INTO(fVulkan12, shaderUniformTexelBufferArrayNonUniformIndexing);
                    DUP_INTO(fVulkan12, shaderStorageTexelBufferArrayNonUniformIndexing);
                    DUP_INTO(fVulkan12, descriptorBindingUniformBufferUpdateAfterBind);
                    DUP_INTO(fVulkan12, descriptorBindingSampledImageUpdateAfterBind);
                    DUP_INTO(fVulkan12, descriptorBindingStorageImageUpdateAfterBind);
                    DUP_INTO(fVulkan12, descriptorBindingStorageBufferUpdateAfterBind);
                    DUP_INTO(fVulkan12, descriptorBindingUniformTexelBufferUpdateAfterBind);
                    DUP_INTO(fVulkan12, descriptorBindingStorageTexelBufferUpdateAfterBind);
                    DUP_INTO(fVulkan12, descriptorBindingUpdateUnusedWhilePending);
                    DUP_INTO(fVulkan12, descriptorBindingPartiallyBound);
                    DUP_INTO(fVulkan12, descriptorBindingVariableDescriptorCount);
                    DUP_INTO(fVulkan12, runtimeDescriptorArray);
                    DUP_INTO(fVulkan12, samplerFilterMinmax);
                    DUP_INTO(fVulkan12, scalarBlockLayout);
                    DUP_INTO(fVulkan12, imagelessFramebuffer);
                    DUP_INTO(fVulkan12, uniformBufferStandardLayout);
                    DUP_INTO(fVulkan12, shaderSubgroupExtendedTypes);
                    DUP_INTO(fVulkan12, separateDepthStencilLayouts);
                    DUP_INTO(fVulkan12, hostQueryReset);
                    DUP_INTO(fVulkan12, timelineSemaphore);
                    DUP_INTO(fVulkan12, bufferDeviceAddress);
                    DUP_INTO(fVulkan12, bufferDeviceAddressCaptureReplay);
                    DUP_INTO(fVulkan12, bufferDeviceAddressMultiDevice);
                    DUP_INTO(fVulkan12, vulkanMemoryModel);
                    DUP_INTO(fVulkan12, vulkanMemoryModelDeviceScope);
                    DUP_INTO(fVulkan12, vulkanMemoryModelAvailabilityVisibilityChains);
                    DUP_INTO(fVulkan12, shaderOutputViewportIndex);
                    DUP_INTO(fVulkan12, shaderOutputLayer);
                    DUP_INTO(fVulkan12, subgroupBroadcastDynamicId);
                }
                break;
            }
            case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_3_FEATURES: {
                auto* features = reinterpret_cast<VkPhysicalDeviceVulkan13Features*>(pNext);
                // If this struct is not chained by Skia, copy enabled features.
                if (features != &fVulkan13) {
                    DUP_INTO(fVulkan13, robustImageAccess);
                    DUP_INTO(fVulkan13, inlineUniformBlock);
                    DUP_INTO(fVulkan13, descriptorBindingInlineUniformBlockUpdateAfterBind);
                    DUP_INTO(fVulkan13, pipelineCreationCacheControl);
                    DUP_INTO(fVulkan13, privateData);
                    DUP_INTO(fVulkan13, shaderDemoteToHelperInvocation);
                    DUP_INTO(fVulkan13, shaderTerminateInvocation);
                    DUP_INTO(fVulkan13, subgroupSizeControl);
                    DUP_INTO(fVulkan13, computeFullSubgroups);
                    DUP_INTO(fVulkan13, synchronization2);
                    DUP_INTO(fVulkan13, textureCompressionASTC_HDR);
                    DUP_INTO(fVulkan13, shaderZeroInitializeWorkgroupMemory);
                    DUP_INTO(fVulkan13, dynamicRendering);
                    DUP_INTO(fVulkan13, shaderIntegerDotProduct);
                    DUP_INTO(fVulkan13, maintenance4);
                }
                break;
            }
            case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_4_FEATURES: {
                auto* features = reinterpret_cast<VkPhysicalDeviceVulkan14Features*>(pNext);
                // If this struct is not chained by Skia, copy enabled features.
                if (features != &fVulkan14) {
                    DUP_INTO(fVulkan14, globalPriorityQuery);
                    DUP_INTO(fVulkan14, shaderSubgroupRotate);
                    DUP_INTO(fVulkan14, shaderSubgroupRotateClustered);
                    DUP_INTO(fVulkan14, shaderFloatControls2);
                    DUP_INTO(fVulkan14, shaderExpectAssume);
                    DUP_INTO(fVulkan14, rectangularLines);
                    DUP_INTO(fVulkan14, bresenhamLines);
                    DUP_INTO(fVulkan14, smoothLines);
                    DUP_INTO(fVulkan14, stippledRectangularLines);
                    DUP_INTO(fVulkan14, stippledBresenhamLines);
                    DUP_INTO(fVulkan14, stippledSmoothLines);
                    DUP_INTO(fVulkan14, vertexAttributeInstanceRateDivisor);
                    DUP_INTO(fVulkan14, vertexAttributeInstanceRateZeroDivisor);
                    DUP_INTO(fVulkan14, indexTypeUint8);
                    DUP_INTO(fVulkan14, dynamicRenderingLocalRead);
                    DUP_INTO(fVulkan14, maintenance5);
                    DUP_INTO(fVulkan14, maintenance6);
                    DUP_INTO(fVulkan14, pipelineProtectedAccess);
                    DUP_INTO(fVulkan14, pipelineRobustness);
                    DUP_INTO(fVulkan14, hostImageCopy);
                    DUP_INTO(fVulkan14, pushDescriptor);
                }
                break;
            }
            case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_RASTERIZATION_ORDER_ATTACHMENT_ACCESS_FEATURES_EXT: {
                chain(newChainEnd, pNext);
                toAdd.fRasterizationOrderAttachmentAccess = false;
                break;
            }
            case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_BLEND_OPERATION_ADVANCED_FEATURES_EXT: {
                chain(newChainEnd, pNext);
                toAdd.fBlendOperationAdvanced = false;
                break;
            }
            case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_EXTENDED_DYNAMIC_STATE_FEATURES_EXT: {
                // Drop this struct if Vulkan 1.3 is enabled
                if (!hasVulkan13Features) {
                    chain(newChainEnd, pNext);

                    // Enable the main feature
                    if (exts.fExtendedDynamicStateEXT) {
                        auto* features =
                                reinterpret_cast<VkPhysicalDeviceExtendedDynamicStateFeaturesEXT*>(
                                        pNext);
                        features->extendedDynamicState = VK_TRUE;
                    }
                }
                toAdd.fExtendedDynamicState = false;
                break;
            }
            case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_EXTENDED_DYNAMIC_STATE_2_FEATURES_EXT: {
                // Drop this struct if Vulkan 1.3 is enabled and the extension's optional features
                // are not (they are not promoted to 1.3).
                auto* features =
                        reinterpret_cast<VkPhysicalDeviceExtendedDynamicState2FeaturesEXT*>(pNext);
                if (!hasVulkan13Features && !features->extendedDynamicState2LogicOp &&
                    !features->extendedDynamicState2PatchControlPoints) {
                    chain(newChainEnd, pNext);

                    // Enable the main feature
                    if (exts.fExtendedDynamicState2EXT) {
                        features->extendedDynamicState2 = VK_TRUE;
                    }
                }
                toAdd.fExtendedDynamicState2 = false;
                break;
            }
            case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VERTEX_INPUT_DYNAMIC_STATE_FEATURES_EXT: {
                chain(newChainEnd, pNext);
                // Enable the main feature
                if (exts.fVertexInputDynamicStateEXT) {
                    auto* features =
                            reinterpret_cast<VkPhysicalDeviceVertexInputDynamicStateFeaturesEXT*>(
                                    pNext);
                    features->vertexInputDynamicState = VK_TRUE;
                }
                toAdd.fVertexInputDynamicState = false;
                break;
            }
            case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_GRAPHICS_PIPELINE_LIBRARY_FEATURES_EXT: {
                chain(newChainEnd, pNext);
                // Enable the main feature
                if (exts.fGraphicsPipelineLibraryEXT) {
                    auto* features =
                            reinterpret_cast<VkPhysicalDeviceGraphicsPipelineLibraryFeaturesEXT*>(
                                    pNext);
                    features->graphicsPipelineLibrary = VK_TRUE;
                }
                toAdd.fGraphicsPipelineLibrary = false;
                break;
            }
            case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SAMPLER_YCBCR_CONVERSION_FEATURES: {
                // Drop this struct if using the Vulkan 1.1 feature struct
                if (!hasVulkan11Features) {
                    chain(newChainEnd, pNext);

                    // Enable the main feature
                    if (exts.fSamplerYcbcrConversionKHR) {
                        auto* features =
                                reinterpret_cast<VkPhysicalDeviceSamplerYcbcrConversionFeatures*>(
                                        pNext);
                        features->samplerYcbcrConversion = VK_TRUE;
                    }
                }
                toAdd.fSamplerYcbcrConversion = false;
                break;
            }
            case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_RGBA10X6_FORMATS_FEATURES_EXT: {
                chain(newChainEnd, pNext);
                toAdd.fRGBA10x6Formats = false;
                break;
            }
            case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SYNCHRONIZATION_2_FEATURES: {
                // Drop this struct if Vulkan 1.3 is enabled
                if (!hasVulkan13Features) {
                    chain(newChainEnd, pNext);
                    // Enable the main feature
                    if (exts.fSynchronization2KHR) {
                        auto* features =
                                reinterpret_cast<VkPhysicalDeviceSynchronization2Features*>(pNext);
                        features->synchronization2 = VK_TRUE;
                    }
                }
                toAdd.fSynchronization2 = false;
                break;
            }
            case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DYNAMIC_RENDERING_FEATURES: {
                // Drop this struct if Vulkan 1.3 is enabled
                if (!hasVulkan13Features) {
                    chain(newChainEnd, pNext);
                    // Enable the main feature
                    if (exts.fDynamicRenderingKHR) {
                        auto* features =
                                reinterpret_cast<VkPhysicalDeviceDynamicRenderingFeatures*>(pNext);
                        features->dynamicRendering = VK_TRUE;
                    }
                }
                toAdd.fDynamicRendering = false;
                break;
            }
            case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DYNAMIC_RENDERING_LOCAL_READ_FEATURES: {
                // Drop this struct if Vulkan 1.4 is enabled
                if (!hasVulkan14Features) {
                    chain(newChainEnd, pNext);
                    // Enable the main feature
                    if (exts.fDynamicRenderingLocalReadKHR) {
                        auto* features = reinterpret_cast<
                                VkPhysicalDeviceDynamicRenderingLocalReadFeatures*>(pNext);
                        features->dynamicRenderingLocalRead = VK_TRUE;
                    }
                    toAdd.fDynamicRenderingLocalRead = false;
                }
                break;
            }
            case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MULTISAMPLED_RENDER_TO_SINGLE_SAMPLED_FEATURES_EXT: {
                chain(newChainEnd, pNext);
                // Enable the main feature
                if (exts.fMultisampledRenderToSingleSampledEXT) {
                    auto* features = reinterpret_cast<
                            VkPhysicalDeviceMultisampledRenderToSingleSampledFeaturesEXT*>(pNext);
                    features->multisampledRenderToSingleSampled = VK_TRUE;
                }
                toAdd.fMultisampledRenderToSingleSampled = false;
                break;
            }
            case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_HOST_IMAGE_COPY_FEATURES: {
                // Drop this struct if Vulkan 1.4 is enabled
                if (!hasVulkan14Features) {
                    chain(newChainEnd, pNext);
                    // Enable the main feature
                    if (exts.fHostImageCopyEXT) {
                        auto* features =
                                reinterpret_cast<VkPhysicalDeviceHostImageCopyFeatures*>(pNext);
                        features->hostImageCopy = VK_TRUE;
                    }
                }
                toAdd.fHostImageCopy = false;
                break;
            }
            case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PIPELINE_CREATION_CACHE_CONTROL_FEATURES: {
                // Drop this struct if Vulkan 1.3 is enabled
                if (!hasVulkan13Features) {
                    chain(newChainEnd, pNext);
                    // Enable the main feature
                    if (exts.fPipelineCreationCacheControlEXT) {
                        auto* features = reinterpret_cast<
                                VkPhysicalDevicePipelineCreationCacheControlFeatures*>(pNext);
                        features->pipelineCreationCacheControl = VK_TRUE;
                    }
                }
                toAdd.fPipelineCreationCacheControl = false;
                break;
            }
            // Gather features promoted to Vulkan 1.1 and drop their structs per the following valid
            // usage statement:
            //
            // If the pNext chain includes a VkPhysicalDeviceVulkan11Features structure, then it
            // must not include a VkPhysicalDevice16BitStorageFeatures,
            // VkPhysicalDeviceMultiviewFeatures, VkPhysicalDeviceVariablePointersFeatures,
            // VkPhysicalDeviceProtectedMemoryFeatures,
            // VkPhysicalDeviceSamplerYcbcrConversionFeatures, or
            // VkPhysicalDeviceShaderDrawParametersFeatures structure
            case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_16BIT_STORAGE_FEATURES: {
                if (hasVulkan11Features) {
                    auto* features = reinterpret_cast<VkPhysicalDevice16BitStorageFeatures*>(pNext);
                    DUP_INTO(fVulkan11, storageBuffer16BitAccess);
                    DUP_INTO(fVulkan11, uniformAndStorageBuffer16BitAccess);
                    DUP_INTO(fVulkan11, storagePushConstant16);
                    DUP_INTO(fVulkan11, storageInputOutput16);
                } else {
                    chain(newChainEnd, pNext);
                }
                break;
            }
            case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MULTIVIEW_FEATURES: {
                if (hasVulkan11Features) {
                    auto* features = reinterpret_cast<VkPhysicalDeviceMultiviewFeatures*>(pNext);
                    DUP_INTO(fVulkan11, multiview);
                    DUP_INTO(fVulkan11, multiviewGeometryShader);
                    DUP_INTO(fVulkan11, multiviewTessellationShader);
                } else {
                    chain(newChainEnd, pNext);
                }
                break;
            }
            case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VARIABLE_POINTERS_FEATURES: {
                if (hasVulkan11Features) {
                    auto* features =
                            reinterpret_cast<VkPhysicalDeviceVariablePointersFeatures*>(pNext);
                    DUP_INTO(fVulkan11, variablePointersStorageBuffer);
                    DUP_INTO(fVulkan11, variablePointers);
                } else {
                    chain(newChainEnd, pNext);
                }
                break;
            }
            case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROTECTED_MEMORY_FEATURES: {
                if (hasVulkan11Features) {
                    auto* features =
                            reinterpret_cast<VkPhysicalDeviceProtectedMemoryFeatures*>(pNext);
                    DUP_INTO(fVulkan11, protectedMemory);
                } else {
                    chain(newChainEnd, pNext);
                }
                break;
            }
            case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_DRAW_PARAMETERS_FEATURES: {
                if (hasVulkan11Features) {
                    auto* features =
                            reinterpret_cast<VkPhysicalDeviceShaderDrawParametersFeatures*>(pNext);
                    DUP_INTO(fVulkan11, shaderDrawParameters);
                } else {
                    chain(newChainEnd, pNext);
                }
                break;
            }

            // Gather features promoted to Vulkan 1.2 and drop their structs per the following valid
            // usage statement:
            //
            // If the pNext chain includes a VkPhysicalDeviceVulkan12Features structure, then it
            // must not include a VkPhysicalDevice8BitStorageFeatures,
            // VkPhysicalDeviceShaderAtomicInt64Features, VkPhysicalDeviceShaderFloat16Int8Features,
            // VkPhysicalDeviceDescriptorIndexingFeatures,
            // VkPhysicalDeviceScalarBlockLayoutFeatures,
            // VkPhysicalDeviceImagelessFramebufferFeatures,
            // VkPhysicalDeviceUniformBufferStandardLayoutFeatures,
            // VkPhysicalDeviceShaderSubgroupExtendedTypesFeatures,
            // VkPhysicalDeviceSeparateDepthStencilLayoutsFeatures,
            // VkPhysicalDeviceHostQueryResetFeatures, VkPhysicalDeviceTimelineSemaphoreFeatures,
            // VkPhysicalDeviceBufferDeviceAddressFeatures, or
            // VkPhysicalDeviceVulkanMemoryModelFeatures structure
            case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_8BIT_STORAGE_FEATURES: {
                if (hasVulkan12Features) {
                    auto* features = reinterpret_cast<VkPhysicalDevice8BitStorageFeatures*>(pNext);
                    DUP_INTO(fVulkan12, storageBuffer8BitAccess);
                    DUP_INTO(fVulkan12, uniformAndStorageBuffer8BitAccess);
                    DUP_INTO(fVulkan12, storagePushConstant8);
                } else {
                    chain(newChainEnd, pNext);
                }
                break;
            }
            case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_ATOMIC_INT64_FEATURES: {
                if (hasVulkan12Features) {
                    auto* features =
                            reinterpret_cast<VkPhysicalDeviceShaderAtomicInt64Features*>(pNext);
                    DUP_INTO(fVulkan12, shaderBufferInt64Atomics);
                    DUP_INTO(fVulkan12, shaderSharedInt64Atomics);
                } else {
                    chain(newChainEnd, pNext);
                }
                break;
            }
            case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_FLOAT16_INT8_FEATURES: {
                if (hasVulkan12Features) {
                    auto* features =
                            reinterpret_cast<VkPhysicalDeviceShaderFloat16Int8Features*>(pNext);
                    DUP_INTO(fVulkan12, shaderFloat16);
                    DUP_INTO(fVulkan12, shaderInt8);
                } else {
                    chain(newChainEnd, pNext);
                }
                break;
            }
            case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DESCRIPTOR_INDEXING_FEATURES: {
                if (hasVulkan12Features) {
                    auto* features =
                            reinterpret_cast<VkPhysicalDeviceDescriptorIndexingFeatures*>(pNext);
                    DUP_INTO(fVulkan12, shaderInputAttachmentArrayDynamicIndexing);
                    DUP_INTO(fVulkan12, shaderUniformTexelBufferArrayDynamicIndexing);
                    DUP_INTO(fVulkan12, shaderStorageTexelBufferArrayDynamicIndexing);
                    DUP_INTO(fVulkan12, shaderUniformBufferArrayNonUniformIndexing);
                    DUP_INTO(fVulkan12, shaderSampledImageArrayNonUniformIndexing);
                    DUP_INTO(fVulkan12, shaderStorageBufferArrayNonUniformIndexing);
                    DUP_INTO(fVulkan12, shaderStorageImageArrayNonUniformIndexing);
                    DUP_INTO(fVulkan12, shaderInputAttachmentArrayNonUniformIndexing);
                    DUP_INTO(fVulkan12, shaderUniformTexelBufferArrayNonUniformIndexing);
                    DUP_INTO(fVulkan12, shaderStorageTexelBufferArrayNonUniformIndexing);
                    DUP_INTO(fVulkan12, descriptorBindingUniformBufferUpdateAfterBind);
                    DUP_INTO(fVulkan12, descriptorBindingSampledImageUpdateAfterBind);
                    DUP_INTO(fVulkan12, descriptorBindingStorageImageUpdateAfterBind);
                    DUP_INTO(fVulkan12, descriptorBindingStorageBufferUpdateAfterBind);
                    DUP_INTO(fVulkan12, descriptorBindingUniformTexelBufferUpdateAfterBind);
                    DUP_INTO(fVulkan12, descriptorBindingStorageTexelBufferUpdateAfterBind);
                    DUP_INTO(fVulkan12, descriptorBindingUpdateUnusedWhilePending);
                    DUP_INTO(fVulkan12, descriptorBindingPartiallyBound);
                    DUP_INTO(fVulkan12, descriptorBindingVariableDescriptorCount);
                    DUP_INTO(fVulkan12, runtimeDescriptorArray);
                } else {
                    chain(newChainEnd, pNext);
                }
                break;
            }
            case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SCALAR_BLOCK_LAYOUT_FEATURES: {
                if (hasVulkan12Features) {
                    auto* features =
                            reinterpret_cast<VkPhysicalDeviceScalarBlockLayoutFeatures*>(pNext);
                    DUP_INTO(fVulkan12, scalarBlockLayout);
                } else {
                    chain(newChainEnd, pNext);
                }
                break;
            }
            case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_IMAGELESS_FRAMEBUFFER_FEATURES: {
                if (hasVulkan12Features) {
                    auto* features =
                            reinterpret_cast<VkPhysicalDeviceImagelessFramebufferFeatures*>(pNext);
                    DUP_INTO(fVulkan12, imagelessFramebuffer);
                } else {
                    chain(newChainEnd, pNext);
                }
                break;
                break;
            }
            case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_UNIFORM_BUFFER_STANDARD_LAYOUT_FEATURES: {
                if (hasVulkan12Features) {
                    auto* features =
                            reinterpret_cast<VkPhysicalDeviceUniformBufferStandardLayoutFeatures*>(
                                    pNext);
                    DUP_INTO(fVulkan12, uniformBufferStandardLayout);
                } else {
                    chain(newChainEnd, pNext);
                }
                break;
            }
            case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_SUBGROUP_EXTENDED_TYPES_FEATURES: {
                if (hasVulkan12Features) {
                    auto* features =
                            reinterpret_cast<VkPhysicalDeviceShaderSubgroupExtendedTypesFeatures*>(
                                    pNext);
                    DUP_INTO(fVulkan12, shaderSubgroupExtendedTypes);
                } else {
                    chain(newChainEnd, pNext);
                }
                break;
            }
            case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SEPARATE_DEPTH_STENCIL_LAYOUTS_FEATURES: {
                if (hasVulkan12Features) {
                    auto* features =
                            reinterpret_cast<VkPhysicalDeviceSeparateDepthStencilLayoutsFeatures*>(
                                    pNext);
                    DUP_INTO(fVulkan12, separateDepthStencilLayouts);
                } else {
                    chain(newChainEnd, pNext);
                }
                break;
            }
            case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_HOST_QUERY_RESET_FEATURES: {
                if (hasVulkan12Features) {
                    auto* features =
                            reinterpret_cast<VkPhysicalDeviceHostQueryResetFeatures*>(pNext);
                    DUP_INTO(fVulkan12, hostQueryReset);
                } else {
                    chain(newChainEnd, pNext);
                }
                break;
            }
            case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_TIMELINE_SEMAPHORE_FEATURES: {
                if (hasVulkan12Features) {
                    auto* features =
                            reinterpret_cast<VkPhysicalDeviceTimelineSemaphoreFeatures*>(pNext);
                    DUP_INTO(fVulkan12, timelineSemaphore);
                } else {
                    chain(newChainEnd, pNext);
                }
                break;
            }
            case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_BUFFER_DEVICE_ADDRESS_FEATURES: {
                if (hasVulkan12Features) {
                    auto* features =
                            reinterpret_cast<VkPhysicalDeviceBufferDeviceAddressFeatures*>(pNext);
                    DUP_INTO(fVulkan12, bufferDeviceAddress);
                    DUP_INTO(fVulkan12, bufferDeviceAddressCaptureReplay);
                    DUP_INTO(fVulkan12, bufferDeviceAddressMultiDevice);
                } else {
                    chain(newChainEnd, pNext);
                }
                break;
            }
            case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_MEMORY_MODEL_FEATURES: {
                if (hasVulkan12Features) {
                    auto* features =
                            reinterpret_cast<VkPhysicalDeviceVulkanMemoryModelFeatures*>(pNext);
                    DUP_INTO(fVulkan12, vulkanMemoryModel);
                    DUP_INTO(fVulkan12, vulkanMemoryModelDeviceScope);
                    DUP_INTO(fVulkan12, vulkanMemoryModelAvailabilityVisibilityChains);
                } else {
                    chain(newChainEnd, pNext);
                }
                break;
            }

            // Gather features promoted to Vulkan 1.3 and drop their structs per the following valid
            // usage statement:
            //
            // If the pNext chain includes a VkPhysicalDeviceVulkan13Features structure, then it
            // must not include a VkPhysicalDeviceDynamicRenderingFeatures,
            // VkPhysicalDeviceImageRobustnessFeatures, VkPhysicalDeviceInlineUniformBlockFeatures,
            // VkPhysicalDeviceMaintenance4Features,
            // VkPhysicalDevicePipelineCreationCacheControlFeatures,
            // VkPhysicalDevicePrivateDataFeatures,
            // VkPhysicalDeviceShaderDemoteToHelperInvocationFeatures,
            // VkPhysicalDeviceShaderIntegerDotProductFeatures,
            // VkPhysicalDeviceShaderTerminateInvocationFeatures,
            // VkPhysicalDeviceSubgroupSizeControlFeatures,
            // VkPhysicalDeviceSynchronization2Features,
            // VkPhysicalDeviceTextureCompressionASTCHDRFeatures, or
            // VkPhysicalDeviceZeroInitializeWorkgroupMemoryFeatures structure
            case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_IMAGE_ROBUSTNESS_FEATURES: {
                if (hasVulkan13Features) {
                    auto* features =
                            reinterpret_cast<VkPhysicalDeviceImageRobustnessFeatures*>(pNext);
                    DUP_INTO(fVulkan13, robustImageAccess);
                } else {
                    chain(newChainEnd, pNext);
                }
                break;
            }
            case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_INLINE_UNIFORM_BLOCK_FEATURES: {
                if (hasVulkan13Features) {
                    auto* features =
                            reinterpret_cast<VkPhysicalDeviceInlineUniformBlockFeatures*>(pNext);
                    DUP_INTO(fVulkan13, inlineUniformBlock);
                    DUP_INTO(fVulkan13, descriptorBindingInlineUniformBlockUpdateAfterBind);
                } else {
                    chain(newChainEnd, pNext);
                }
                break;
            }
            case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MAINTENANCE_4_FEATURES: {
                if (hasVulkan13Features) {
                    auto* features = reinterpret_cast<VkPhysicalDeviceMaintenance4Features*>(pNext);
                    DUP_INTO(fVulkan13, maintenance4);
                } else {
                    chain(newChainEnd, pNext);
                }
                break;
            }
            case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PRIVATE_DATA_FEATURES: {
                if (hasVulkan13Features) {
                    auto* features = reinterpret_cast<VkPhysicalDevicePrivateDataFeatures*>(pNext);
                    DUP_INTO(fVulkan13, privateData);
                } else {
                    chain(newChainEnd, pNext);
                }
                break;
            }
            case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_DEMOTE_TO_HELPER_INVOCATION_FEATURES: {
                if (hasVulkan13Features) {
                    auto* features = reinterpret_cast<
                            VkPhysicalDeviceShaderDemoteToHelperInvocationFeatures*>(pNext);
                    DUP_INTO(fVulkan13, shaderDemoteToHelperInvocation);
                } else {
                    chain(newChainEnd, pNext);
                }
                break;
            }
            case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_INTEGER_DOT_PRODUCT_FEATURES: {
                if (hasVulkan13Features) {
                    auto* features =
                            reinterpret_cast<VkPhysicalDeviceShaderIntegerDotProductFeatures*>(
                                    pNext);
                    DUP_INTO(fVulkan13, shaderIntegerDotProduct);
                } else {
                    chain(newChainEnd, pNext);
                }
                break;
            }
            case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_TERMINATE_INVOCATION_FEATURES: {
                if (hasVulkan13Features) {
                    auto* features =
                            reinterpret_cast<VkPhysicalDeviceShaderTerminateInvocationFeatures*>(
                                    pNext);
                    DUP_INTO(fVulkan13, shaderTerminateInvocation);
                } else {
                    chain(newChainEnd, pNext);
                }
                break;
            }
            case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_SIZE_CONTROL_FEATURES: {
                if (hasVulkan13Features) {
                    auto* features =
                            reinterpret_cast<VkPhysicalDeviceSubgroupSizeControlFeatures*>(pNext);
                    DUP_INTO(fVulkan13, subgroupSizeControl);
                    DUP_INTO(fVulkan13, computeFullSubgroups);
                } else {
                    chain(newChainEnd, pNext);
                }
                break;
            }
            case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_TEXTURE_COMPRESSION_ASTC_HDR_FEATURES: {
                if (hasVulkan13Features) {
                    auto* features =
                            reinterpret_cast<VkPhysicalDeviceTextureCompressionASTCHDRFeatures*>(
                                    pNext);
                    DUP_INTO(fVulkan13, textureCompressionASTC_HDR);
                } else {
                    chain(newChainEnd, pNext);
                }
                break;
            }
            case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ZERO_INITIALIZE_WORKGROUP_MEMORY_FEATURES: {
                if (hasVulkan13Features) {
                    auto* features = reinterpret_cast<
                            VkPhysicalDeviceZeroInitializeWorkgroupMemoryFeatures*>(pNext);
                    DUP_INTO(fVulkan13, shaderZeroInitializeWorkgroupMemory);
                } else {
                    chain(newChainEnd, pNext);
                }
                break;
            }

            // Gather features promoted to Vulkan 1.4 and drop their structs per the following valid
            // usage statement:
            //
            // If the pNext chain includes a VkPhysicalDeviceVulkan14Features structure, then it
            // must not include a VkPhysicalDeviceGlobalPriorityQueryFeatures,
            // VkPhysicalDeviceShaderSubgroupRotateFeatures,
            // VkPhysicalDeviceShaderFloatControls2Features,
            // VkPhysicalDeviceShaderExpectAssumeFeatures,
            // VkPhysicalDeviceLineRasterizationFeatures,
            // VkPhysicalDeviceVertexAttributeDivisorFeatures,
            // VkPhysicalDeviceIndexTypeUint8Features,
            // VkPhysicalDeviceDynamicRenderingLocalReadFeatures,
            // VkPhysicalDeviceMaintenance5Features, VkPhysicalDeviceMaintenance6Features,
            // VkPhysicalDevicePipelineProtectedAccessFeatures,
            // VkPhysicalDevicePipelineRobustnessFeatures, or VkPhysicalDeviceHostImageCopyFeatures
            // structure
            case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_GLOBAL_PRIORITY_QUERY_FEATURES: {
                if (hasVulkan14Features) {
                    auto* features =
                            reinterpret_cast<VkPhysicalDeviceGlobalPriorityQueryFeatures*>(pNext);
                    DUP_INTO(fVulkan14, globalPriorityQuery);
                } else {
                    chain(newChainEnd, pNext);
                }
                break;
            }
            case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_SUBGROUP_ROTATE_FEATURES: {
                if (hasVulkan14Features) {
                    auto* features =
                            reinterpret_cast<VkPhysicalDeviceShaderSubgroupRotateFeatures*>(pNext);
                    DUP_INTO(fVulkan14, shaderSubgroupRotate);
                    DUP_INTO(fVulkan14, shaderSubgroupRotateClustered);
                } else {
                    chain(newChainEnd, pNext);
                }
                break;
            }
            case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_FLOAT_CONTROLS_2_FEATURES: {
                if (hasVulkan14Features) {
                    auto* features =
                            reinterpret_cast<VkPhysicalDeviceShaderFloatControls2Features*>(pNext);
                    DUP_INTO(fVulkan14, shaderFloatControls2);
                } else {
                    chain(newChainEnd, pNext);
                }
                break;
            }
            case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_EXPECT_ASSUME_FEATURES: {
                if (hasVulkan14Features) {
                    auto* features =
                            reinterpret_cast<VkPhysicalDeviceShaderExpectAssumeFeatures*>(pNext);
                    DUP_INTO(fVulkan14, shaderExpectAssume);
                } else {
                    chain(newChainEnd, pNext);
                }
                break;
            }
            case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_LINE_RASTERIZATION_FEATURES: {
                if (hasVulkan14Features) {
                    auto* features =
                            reinterpret_cast<VkPhysicalDeviceLineRasterizationFeatures*>(pNext);
                    DUP_INTO(fVulkan14, rectangularLines);
                    DUP_INTO(fVulkan14, bresenhamLines);
                    DUP_INTO(fVulkan14, smoothLines);
                    DUP_INTO(fVulkan14, stippledRectangularLines);
                    DUP_INTO(fVulkan14, stippledBresenhamLines);
                    DUP_INTO(fVulkan14, stippledSmoothLines);
                } else {
                    chain(newChainEnd, pNext);
                }
                break;
            }
            case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VERTEX_ATTRIBUTE_DIVISOR_FEATURES: {
                if (hasVulkan14Features) {
                    auto* features =
                            reinterpret_cast<VkPhysicalDeviceVertexAttributeDivisorFeatures*>(
                                    pNext);
                    DUP_INTO(fVulkan14, vertexAttributeInstanceRateDivisor);
                    DUP_INTO(fVulkan14, vertexAttributeInstanceRateZeroDivisor);
                } else {
                    chain(newChainEnd, pNext);
                }
                break;
            }
            case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_INDEX_TYPE_UINT8_FEATURES: {
                if (hasVulkan14Features) {
                    auto* features =
                            reinterpret_cast<VkPhysicalDeviceIndexTypeUint8Features*>(pNext);
                    DUP_INTO(fVulkan14, indexTypeUint8);
                } else {
                    chain(newChainEnd, pNext);
                }
                break;
            }
            case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MAINTENANCE_5_FEATURES: {
                if (hasVulkan14Features) {
                    auto* features = reinterpret_cast<VkPhysicalDeviceMaintenance5Features*>(pNext);
                    DUP_INTO(fVulkan14, maintenance5);
                } else {
                    chain(newChainEnd, pNext);
                }
                break;
            }
            case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MAINTENANCE_6_FEATURES: {
                if (hasVulkan14Features) {
                    auto* features = reinterpret_cast<VkPhysicalDeviceMaintenance6Features*>(pNext);
                    DUP_INTO(fVulkan14, maintenance6);
                } else {
                    chain(newChainEnd, pNext);
                }
                break;
            }
            case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PIPELINE_PROTECTED_ACCESS_FEATURES: {
                if (hasVulkan14Features) {
                    auto* features =
                            reinterpret_cast<VkPhysicalDevicePipelineProtectedAccessFeatures*>(
                                    pNext);
                    DUP_INTO(fVulkan14, pipelineProtectedAccess);
                } else {
                    chain(newChainEnd, pNext);
                }
                break;
            }
            case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PIPELINE_ROBUSTNESS_FEATURES: {
                if (hasVulkan14Features) {
                    auto* features =
                            reinterpret_cast<VkPhysicalDevicePipelineRobustnessFeatures*>(pNext);
                    DUP_INTO(fVulkan14, pipelineRobustness);
                } else {
                    chain(newChainEnd, pNext);
                }
                break;
            }

            default:
                // Retain everything else that's chained.
                chain(newChainEnd, pNext);
                break;
        }

        pNext = pNext->pNext;
    }
#undef DUP_INTO

    // Chain any struct that was not found in appFeatures.
    if (hasVulkan11Features) {
        chain(newChainEnd, &fVulkan11);
    }
    if (hasVulkan12Features) {
        chain(newChainEnd, &fVulkan12);
    }
    if (hasVulkan13Features) {
        chain(newChainEnd, &fVulkan13);
    }
    if (hasVulkan14Features) {
        chain(newChainEnd, &fVulkan14);
    }
    if (toAdd.fRasterizationOrderAttachmentAccess) {
        chain(newChainEnd, &fRasterizationOrderAttachmentAccess);
    }
    if (toAdd.fBlendOperationAdvanced) {
        chain(newChainEnd, &fBlendOperationAdvanced);
    }
    if (toAdd.fExtendedDynamicState) {
        chain(newChainEnd, &fExtendedDynamicState);
    }
    if (toAdd.fExtendedDynamicState2) {
        chain(newChainEnd, &fExtendedDynamicState2);
    }
    if (toAdd.fVertexInputDynamicState) {
        chain(newChainEnd, &fVertexInputDynamicState);
    }
    if (toAdd.fGraphicsPipelineLibrary) {
        chain(newChainEnd, &fGraphicsPipelineLibrary);
    }
    if (toAdd.fSamplerYcbcrConversion) {
        chain(newChainEnd, &fSamplerYcbcrConversion);
    }
    if (toAdd.fRGBA10x6Formats) {
        chain(newChainEnd, &fRGBA10x6Formats);
    }
    if (toAdd.fSynchronization2) {
        chain(newChainEnd, &fSynchronization2);
    }
    if (toAdd.fDynamicRendering) {
        chain(newChainEnd, &fDynamicRendering);
    }
    if (toAdd.fDynamicRenderingLocalRead) {
        chain(newChainEnd, &fDynamicRenderingLocalRead);
    }
    if (toAdd.fMultisampledRenderToSingleSampled) {
        chain(newChainEnd, &fMultisampledRenderToSingleSampled);
    }
    if (toAdd.fHostImageCopy) {
        chain(newChainEnd, &fHostImageCopy);
    }
    if (toAdd.fPipelineCreationCacheControl) {
        chain(newChainEnd, &fPipelineCreationCacheControl);
    }

    // Replace appFeatures' pNext with the new chain.
    *newChainEnd = nullptr;
    appFeatures.pNext = newChainStart;

    fHasAddedFeaturesToEnable = true;
}

}  // namespace skgpu
