diff --git a/src/include/libplacebo/vulkan.h b/src/include/libplacebo/vulkan.h
index 62255c5d938a9dc929e49e73f65acc7dcd410de7..c5ef9b154af82e8d80925b0e2d4824e4ccc00b26 100644
--- a/src/include/libplacebo/vulkan.h
+++ b/src/include/libplacebo/vulkan.h
@@ -125,9 +125,6 @@ struct pl_vulkan {
     int num_extensions;
 
     // The device features that were enabled at device creation time.
-    //
-    // Note: The pNext chain of this only includes features known to
-    // libplacebo, which as the time of writing only includes the base set.
     const VkPhysicalDeviceFeatures2KHR *features;
 
     // The explicit queue families we are using to provide a given capability,
@@ -235,7 +232,9 @@ struct pl_vulkan_params {
     // Optional extra features to enable at device creation time. These are
     // opportunistically enabled if supported by the physical device, but
     // otherwise kept disabled. Users may include extra extension-specific
-    // features in the pNext chain.
+    // features in the pNext chain, however these *must* all be
+    // extension-specific structs, i.e. the use of "meta-structs" like
+    // VkPhysicalDeviceVulkan11Features is not allowed.
     const VkPhysicalDeviceFeatures2KHR *features;
 
     // --- Misc/debugging options
@@ -456,9 +455,9 @@ const struct pl_tex *pl_vulkan_wrap(const struct pl_gpu *gpu,
 // optional, but provide a hint to the API user as to what might be worth
 // enabling at device creation time.
 //
-// Note: Currently, libplacebo does not include any extra extension-specific
-// device features in the `pNext` chain of pl_vulkan_recommended_features, but
-// this may change in the future.
+// Note: This also includes physical device features provided by extensions.
+// They are all provided using extension-specific features structs, rather
+// than the more general purpose VkPhysicalDeviceVulkan11Features etc.
 extern const char * const pl_vulkan_recommended_extensions[];
 extern const int pl_vulkan_num_recommended_extensions;
 extern const VkPhysicalDeviceFeatures2KHR pl_vulkan_recommended_features;
diff --git a/src/vulkan/context.c b/src/vulkan/context.c
index 0445b07f046898244c653896d5d85720495b80b0..89bef52118d482b37fe1c1c8ef145e117c9016c9 100644
--- a/src/vulkan/context.c
+++ b/src/vulkan/context.c
@@ -1087,21 +1087,50 @@ static bool device_init(struct vk_ctx *vk, const struct pl_vulkan_params *params
         }
     }
 
-    // Enable all features that we might need, by iterating through the entire
-    // VkPhysicalDeviceFeatures struct as a VkBool32 array and checking each
-    // supported feature against the whitelist of features we want
-    vk->GetPhysicalDeviceFeatures2KHR(vk->physd, &vk->features);
-    for (int i = 0; i < sizeof(VkPhysicalDeviceFeatures) / sizeof(VkBool32); i++) {
-        VkBool32 wanted = ((VkBool32 *) &pl_vulkan_recommended_features.features)[i];
-        if (params->features)
-            wanted |= ((VkBool32 *) &params->features->features)[i];
+    // Query all supported device features by constructing a pNext chain
+    // starting with the features we care about and ending with whatever
+    // features were requested by the user
+    vk->features.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2_KHR;
+    for (const VkBaseInStructure *in = pl_vulkan_recommended_features.pNext;
+            in; in = in->pNext)
+        vk_link_struct(&vk->features, vk_struct_memdup(vk->ta, in));
+
+    for (const VkBaseInStructure *in = (const VkBaseInStructure *) params->features;
+            in; in = in->pNext)
+    {
+        if (vk_find_struct(&vk->features, in->sType))
+            continue; // skip structs already present
+
+        void *copy = vk_struct_memdup(vk->ta, in);
+        if (!copy) {
+            PL_ERR(vk, "Unknown struct type %"PRIu64"?", (uint64_t) in->sType);
+            continue;
+        }
 
-        ((VkBool32 *) &vk->features.features)[i] &= wanted;
+        vk_link_struct(&vk->features, copy);
     }
 
-    // Temporarily link the pNext chain of the extra user features into this
-    if (params->features)
-        vk->features.pNext = params->features->pNext;
+    vk->GetPhysicalDeviceFeatures2KHR(vk->physd, &vk->features);
+
+    // Go through the features chain a second time and mask every option
+    // that wasn't whitelisted by either libplacebo or the user
+    for (VkBaseOutStructure *chain = (VkBaseOutStructure *) &vk->features;
+            chain; chain = chain->pNext)
+    {
+        const VkBaseInStructure *in_a, *in_b;
+        in_a = vk_find_struct(&pl_vulkan_recommended_features, chain->sType);
+        in_b = vk_find_struct(params->features, chain->sType);
+        in_a = PL_DEF(in_a, in_b);
+        in_b = PL_DEF(in_b, in_a);
+        pl_assert(in_a && in_b);
+
+        VkBool32 *req = (VkBool32 *) &chain[1];
+        const VkBool32 *wl_a = (const VkBool32 *) &in_a[1];
+        const VkBool32 *wl_b = (const VkBool32 *) &in_b[1];
+        size_t size = vk_struct_size(chain->sType) - sizeof(chain[0]);
+        for (int i = 0; i < size / sizeof(VkBool32); i++)
+            req[i] &= wl_a[i] || wl_b[i];
+    }
 
     VkDeviceCreateInfo dinfo = {
         .sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO,
@@ -1117,7 +1146,6 @@ static bool device_init(struct vk_ctx *vk, const struct pl_vulkan_params *params
         PL_INFO(vk, "    %s", (*exts)[i]);
 
     VK(vk->CreateDevice(vk->physd, &dinfo, VK_ALLOC, &vk->dev));
-    vk->features.pNext = NULL;
 
     // Load all mandatory device-level functions
     for (int i = 0; i < PL_ARRAY_SIZE(vk_dev_funs); i++) {
@@ -1186,7 +1214,6 @@ const struct pl_vulkan *pl_vulkan_create(struct pl_context *ctx,
         .ctx = ctx,
         .inst = params->instance,
         .GetInstanceProcAddr = get_proc_addr_fallback(ctx, params->get_proc_addr),
-        .features.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2_KHR,
     };
 
     if (!vk->GetInstanceProcAddr)
@@ -1337,7 +1364,6 @@ const struct pl_vulkan *pl_vulkan_import(struct pl_context *ctx,
         .physd = params->phys_device,
         .dev = params->device,
         .GetInstanceProcAddr = get_proc_addr_fallback(ctx, params->get_proc_addr),
-        .features.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2_KHR,
     };
 
     if (!vk->GetInstanceProcAddr)
@@ -1377,11 +1403,10 @@ const struct pl_vulkan *pl_vulkan_import(struct pl_context *ctx,
                 PRINTF_VER(params->max_api_version), PRINTF_VER(vk->api_ver));
     }
 
-    if (params->features) {
-        // Explicitly drop the pNext chain of this, since we don't currently
-        // "officially" know about any extra features provided by extensions
-        vk->features.features = params->features->features;
-    }
+    VkPhysicalDeviceFeatures2KHR *features;
+    features = vk_chain_memdup(vk->ta, params->features);
+    if (features)
+        vk->features = *features;
 
     // Load all mandatory device-level functions
     for (int i = 0; i < PL_ARRAY_SIZE(vk_dev_funs); i++) {
diff --git a/src/vulkan/utils.c b/src/vulkan/utils.c
index 0d1ae2c26ff6896940f9d8d0bf7c69694117057d..2a84d906d1ff0e178244b9d5df53306f4946c932 100644
--- a/src/vulkan/utils.c
+++ b/src/vulkan/utils.c
@@ -103,3 +103,54 @@ const enum pl_handle_type vk_sync_handle_list[] = {
 #endif
         0
 };
+
+const void *vk_find_struct(const void *chain, VkStructureType stype)
+{
+    const VkBaseInStructure *in = chain;
+    while (in) {
+        if (in->sType == stype)
+            return in;
+
+        in = in->pNext;
+    }
+
+    return NULL;
+}
+
+void vk_link_struct(void *chain, void *in)
+{
+    if (!in)
+        return;
+
+    VkBaseOutStructure *out = chain;
+    while (out->pNext)
+        out = out->pNext;
+
+    out->pNext = in;
+}
+
+void *vk_struct_memdup(void *tactx, const void *pin)
+{
+    if (!pin)
+        return NULL;
+
+    const VkBaseInStructure *in = pin;
+    size_t size = vk_struct_size(in->sType);
+    if (!size)
+        return NULL;
+
+    VkBaseOutStructure *out = talloc_memdup(tactx, in, size);
+    out->pNext = NULL;
+    return out;
+}
+
+void *vk_chain_memdup(void *tactx, const void *pin)
+{
+    const VkBaseInStructure *in = pin;
+    VkBaseOutStructure *out = vk_struct_memdup(tactx, in);
+    if (!out)
+        return NULL;
+
+    out->pNext = vk_chain_memdup(tactx, in->pNext);
+    return out;
+}
diff --git a/src/vulkan/utils.h b/src/vulkan/utils.h
index a017cd434f99d39cd3ace06043805058e3545aff..56261f99591bc76e6770fb40bc15729ddf682a45 100644
--- a/src/vulkan/utils.h
+++ b/src/vulkan/utils.h
@@ -23,6 +23,9 @@
 const char *vk_res_str(VkResult res);
 const char *vk_obj_str(VkObjectType obj);
 
+// Return the size of an arbitrary vulkan struct. Returns 0 for unknown structs
+size_t vk_struct_size(VkStructureType stype);
+
 // Enum translation boilerplate
 VkExternalMemoryHandleTypeFlagBitsKHR vk_mem_handle_type(enum pl_handle_type);
 VkExternalSemaphoreHandleTypeFlagBitsKHR vk_sync_handle_type(enum pl_handle_type);
@@ -36,6 +39,18 @@ bool vk_external_mem_check(const VkExternalMemoryPropertiesKHR *props,
 extern const enum pl_handle_type vk_mem_handle_list[];
 extern const enum pl_handle_type vk_sync_handle_list[];
 
+// Find a structure in a pNext chain, or NULL
+const void *vk_find_struct(const void *chain, VkStructureType stype);
+
+// Link a structure into a pNext chain
+void vk_link_struct(void *chain, void *in);
+
+// Make a copy of a structure, not including the pNext chain
+void *vk_struct_memdup(void *tactx, const void *in);
+
+// Make a deep copy of an entire pNext chain
+void *vk_chain_memdup(void *tactx, const void *in);
+
 // Convenience macros to simplify a lot of common boilerplate
 #define VK_ASSERT(res, str)                               \
     do {                                                  \
diff --git a/src/vulkan/utils_gen.py b/src/vulkan/utils_gen.py
index cf3b3fb8d9b0137d187678bdb4af51603727afcb..07d3a5f8eb1234cf191bef3fd2011d0c0d11287e 100644
--- a/src/vulkan/utils_gen.py
+++ b/src/vulkan/utils_gen.py
@@ -43,6 +43,17 @@ const char *vk_obj_str(VkObjectType obj)
     default: return "unknown object";
     }
 }
+
+size_t vk_struct_size(VkStructureType stype)
+{
+    switch (stype) {
+%for struct in vkstructs:
+    case ${struct.stype}: return sizeof(${struct.name});
+%endfor
+
+    default: return 0;
+    }
+}
 """)
 
 class Obj(object):
@@ -59,6 +70,22 @@ def get_vkobjects(registry):
             yield Obj(enum = e.attrib['name'],
                       name = e.attrib['comment'])
 
+def get_vkstructs(registry):
+    for e in registry.findall('types/type[@category="struct"]'):
+        # Strings for platform-specific crap we want to blacklist as they will
+        # most likely cause build failures
+        blacklist_strs = [
+            'ANDROID', 'Surface', 'Win32', 'D3D12', 'GGP'
+        ]
+
+        if any([ str in e.attrib['name'] for str in blacklist_strs ]):
+            continue
+
+        stype = e.find('member/name[.="sType"]/..')
+        if stype and 'values' in stype.attrib:
+            yield Obj(stype = stype.attrib['values'],
+                      name = e.attrib['name'])
+
 if __name__ == '__main__':
     assert len(sys.argv) == 3
     xmlfile = sys.argv[1]
@@ -69,4 +96,5 @@ if __name__ == '__main__':
         f.write(TEMPLATE.render(
             vkresults = get_vkresults(registry),
             vkobjects = get_vkobjects(registry),
+            vkstructs = get_vkstructs(registry),
         ))