Modular Vulkan feature and extension manager

Recently I've been trying to squeeze a few hours at a time from my weekends to work on my Vulkan renderer projects. Because the extremely limited time I have, any feature I want need to planned well and get implemented in a few hours or I need to break it down to do so. One of such feature I've want to implement is a modular Vulkan Feature management.

Root Issue: hard-coding enabled features

When creating an Vulkan device, you have a lot of options which extensions you want to enable, which feature you want to enable. This is done by inserting features into the pNext chain and extensions into ppEnabledExtenionNames.

typedef struct VkDeviceCreateInfo {
    VkStructureType                    sType;
    const void*                        pNext;
    VkDeviceCreateFlags                flags;
    uint32_t                           queueCreateInfoCount;
    const VkDeviceQueueCreateInfo*     pQueueCreateInfos;
    uint32_t                           enabledLayerCount;
    const char* const*                 ppEnabledLayerNames;
    uint32_t                           enabledExtensionCount;
    const char* const*                 ppEnabledExtensionNames;
    const VkPhysicalDeviceFeatures*    pEnabledFeatures;
} VkDeviceCreateInfo;

Issue is that you also need to query those features before you enabling them. Some device many supports RayTracingPipeline some may not. Hard coding everything in the device creation code often leads to non-portable binaries. It works on your desktop GPU, then you check out the code on your laptop, it doesn't run anymore.

It has been like this for me for a long time I've want to resolve it once for all. Clearly we need some abstraction on top of it.

Abstracting an interface of Vulkan features

For extensions it's relatively easy, you just passing as array of const char*, features on the other hand are more of headache.

In the end, we need to chain those features together.

  VkDeviceCreateInfo info{};
  //some info setup code.


  VkPhysicalDeviceFeatures2 feats{};
  feats.features.somefatures0 = VK_TRUE;
  //enabling other features
  info.pNext = &feats;

  //Second feature struct
  VkPhysicalDeviceVulkan11Features feats11 {};
  feats.pNext = &feats11;
  feats11.somefeatures0 = VK_TRUE;
  //enabling other features.

  //this goes on until your have chain all the feature structures.
  VkPhysicalDeviceSomeFeatures featsN {};
  featsN_1.pNext = &featsN;
  featsN.pNext = nullptr; //the last one.

Apparently if we want to do this every single time it will be a lot of boring and typing. Note to mention you have to check those features availability before putting it in VkDeviceCreateInfo. There must be a better way doing it, right?

Taking advantage of T::* type in C++

The issue here is there is too many VkPhysicalDeviceSomeFeatures types in Vulkan. Creating an interface contains all those types would be insane, and I have only a few hours. However all those VkPhysicalDeviceSomeFeatures looks similar:

  typedef struct VkPhysicalDeviceFeatures {
      VkBool32    robustBufferAccess;
      VkBool32    fullDrawIndexUint32;
      VkBool32    imageCubeArray;
      VkBool32    independentBlend;
      VkBool32    geometryShader;
      VkBool32    tessellationShader;
      VkBool32    sampleRateShading;
      //...
  };

They are merely just list of VkBool32. After experimenting a bit. I found that I can simply write a templated function to enable one feature using T::* pointer.

  template<FEAT> //FEAT is a type like VkPhysicalDeviceFeatures
  feature<FEAT>& feature::require(vk::Bool32 FEATS::*feat)
  {
          //for enabling the feature
          m_feats.*feat = true;
          //for query the feature.
          m_requests.emplace_back([feat](FEATS& feats) -> bool
          { return feats.*feat == true; });
  
          return *this;
  }

The feature class is something like this.

   //FEATS is some type like VkPhysicalDeviceFeatures
  template <typename FEATS>
  struct feature
  {
          //actual features stores here
          FEATS                                    m_feats;
          //query fucntions
          std::vector<std::function<bool(FEATS&)>> m_requests;

          //for chaining the features together.
          void** pnext() override { return &m_feats.pNext; }
  };

like in require(), at feature query time I will need to check against my feature query std::functions, and at device creation I can append those features together.

Feature Manager class

On top of this I need a manager interface to manage all the features together. I choose to implement like this

  class feature_descriptor
  {
  public:
          feature_descriptor() noexcept;

          bool check(vk::PhysicalDevice phy_dev) const;
          feature_descriptor& require_extension(char* extension);
          template <class FEATS>
          feature<FEATS>& require_feature()
          {
                  std::unique_ptr<feature<FEATS>> feats =
                          std::make_unique<feature<FEATS>>();
                  //code to please inside feature_descriptor
                  //...
                  return *static_cast<feature<FEATS>*>(feats);
          }
  };

In this way I can request a feature pack such as vk::PhysicalDeviceVulkan12Features and enable each feature like this:

  //enabling Vulkan 1.2 features
  desc.require_feature<vk::PhysicalDeviceVulkan12Features>()
      .require(&vk::PhysicalDeviceVulkan12Features::timelineSemaphore)
      .require(&vk::PhysicalDeviceVulkan12Features::bufferDeviceAddress);

  //enabling Vulkan 1.3 features
  desc.require_feature<vk::PhysicalDeviceVulkan13Features>()
      .require(&vk::PhysicalDeviceVulkan13Features::dynamicRendering);

Much easier right 😄?

comments powered by Disqus