Skip to content

RFC: Vulkan structure defaulting for C++11#2669

Draft
pdaniell-nv wants to merge 5 commits intomainfrom
struct-defaulting
Draft

RFC: Vulkan structure defaulting for C++11#2669
pdaniell-nv wants to merge 5 commits intomainfrom
struct-defaulting

Conversation

@pdaniell-nv
Copy link
Copy Markdown

@pdaniell-nv pdaniell-nv commented Jan 30, 2026

This PR adds optional Vulkan structure C++11 default member initializations that apps compiling for C++11 and above can use to avoid having to initialize the sType, pNext and other member values manually. The output of these changes can be seen here:

The initial draft PR just supports sType, pNext and lineWidth. Defaults for other members will be added soon.

This PR adds a new C macro VK_CPP11_DEFAULT(value) and is added to Vulkan structures where default values make sense and add convenience for application. This especially applies to the sType and pNext members. For example:

typedef struct VkPhysicalDeviceProperties2 {
    VkStructureType               sType VK_CPP11_DEFAULT(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2);
    void*                         pNext VK_CPP11_DEFAULT(nullptr);
    VkPhysicalDeviceProperties    properties;
} VkPhysicalDeviceProperties2;

In this example, if the application declares a VkPhysicalDeviceProperties2 variable like this:

    VkPhysicalDeviceProperties2 properties;

then the sType and pNext members will be initialized, making this a correctly initialized structure that can be used directly with the Vulkan API.

The application can also do the following to zero-initialize all the other members:

    VkPhysicalDeviceProperties2 properties{ };

When using C++20, the applications can combine this functionality with designated initializers like this:

    VkPhysicalDeviceVulkan14Properties vulkan14Properties{ };
    VkPhysicalDeviceProperties2 properties{ .pNext{&vulkan14Properties} };
    vkGetPhysicalDeviceProperties2(physicalDevice, &properties);

Structure members that do not have a VK_CPP11_DEFAULT initializer should be manually set by the application like before because there is no appropriate default.

@pdaniell-nv pdaniell-nv marked this pull request as draft January 30, 2026 23:55
@r-potter r-potter changed the title Draft: Vulkan structure defaulting for C++11+ RFC: Vulkan structure defaulting for C++11+ Feb 3, 2026
@pdaniell-nv pdaniell-nv changed the title RFC: Vulkan structure defaulting for C++11+ RFC: Vulkan structure defaulting for C++11 Feb 3, 2026
pdaniell-nv added a commit to KhronosGroup/Vulkan-Headers that referenced this pull request Feb 3, 2026
These headers are generated from the updates here: KhronosGroup/Vulkan-Docs#2669
@ShabbyX
Copy link
Copy Markdown
Contributor

ShabbyX commented Feb 4, 2026

If you'd like to add C99 support for defaults, here's an idea:

#define VK_PHYSICAL_DEVICE_VULKAN_1_4_PROPERTIES_DEFAULTS \
    .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_4_PROPERTIES, \
    .pNext = NULL,

Then one could do this, which is no more onerous than the existing need for setting the sType (it's a little shorter even):

    VkPhysicalDeviceVulkan14Properties vulkan14Properties{
        VK_PHYSICAL_DEVICE_VULKAN_1_4_PROPERTIES_DEFAULTS,
    };
    VkPhysicalDeviceProperties2 properties{
        VK_PHYSICAL_DEVICE_PROPERTIES_DEFAULTS,
        .pNext = &vulkan14Properties,
    };
    vkGetPhysicalDeviceProperties2(physicalDevice, &properties);

Note that C99 allows .field being specified multiple times, the last one will take effect.

@r-potter r-potter added the Request for Comment Requests for community feedback label Feb 5, 2026
@r-potter
Copy link
Copy Markdown
Contributor

r-potter commented Feb 5, 2026

If we adopt this, we probably need to start being explicit about what language our samples and other code snippets (such as proposal docs) are written in.

This compiles in both C and C++, but would violate valid usage in C. VVL will catch it, but it is a potential footgun for anyone blindly copying example code into C files. That danger is slightly mitigated if the sample code also contains other C++-isms, and so fails to compile anyway.

VkPhysicalDeviceProperties2 properties;
vkGetPhysicalDeviceProperties2(physicalDevice, &properties);
assert(properties.sType == VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2);

I don't think this is a deal breaker in any way, just a reminder for our future selves to make sure that documentation/proposal docs are clear on this.

@charles-lunarg
Copy link
Copy Markdown

Not an exhaustive list, but here are some "sane" defaults that was discussed in the Graphics Programming Discord server (vulkan channel)

Some of these are merely common values rather than "true" defaults, but this is to get things moving, not the final list.

  • VkViewport::maxDepth = 1.0f
  • VkPipelineRasterizationStateCreateInfo::lineWidth = 1.0f
  • VkPipelineColorBlendAttachmentState::colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT;
  • VkImageCreateInfo::mipLevels = 1;
  • VkImageCreateInfo::arrayLayers = 1;
  • VkImageCreateInfo::samples = VK_SAMPLE_COUNT_1_BIT;

@charles-lunarg
Copy link
Copy Markdown

Looking at the generated code in Vulkan-Headers, it shows that the function overloads that take C++20 spans requires VK_NO_PROTOTYPES to not be defined. Put another way, these are only usable without dynamically linking to the Vulkan-Loader, meaning users of Volk (or a custom function loader) could not use these overloads.

Seems like a prickly thorn to work around. Can't define overloads for functions that do not exist, nor do we want to add more function overloads.

@pdaniell-nv
Copy link
Copy Markdown
Author

Looking at the generated code in Vulkan-Headers, it shows that the function overloads that take C++20 spans requires VK_NO_PROTOTYPES to not be defined. Put another way, these are only usable without dynamically linking to the Vulkan-Loader, meaning users of Volk (or a custom function loader) could not use these overloads.

Seems like a prickly thorn to work around. Can't define overloads for functions that do not exist, nor do we want to add more function overloads.

Because Volk declares the "functions" as global function pointer variables, you can't use regular C++ function overloading. You can only overload regular functions. This is partly why I created KhronosGroup/Vulkan-Loader#1850 so vulkan-1.lib implements more entry points as regular functions.

I'm not yet sure how best to workaround this problem with Volk and other similar utilities. namespaces kind of works, but you can't use using namespace vkcpp; as you return to the same problem. We could give all the overloads a special prefix or suffix, but that is also not ideal if the goal is to make these helpers seamless.

@charles-lunarg
Copy link
Copy Markdown

If these overloads require linking, them I'm not likely to use them nor advocate for them as one of the benefits of not linking is the ability to get the driver's functions and call those directly (or whatever the first layer in the chain is).

Vulkan-hpp has many examples of overloads without requiring linking, this is because they implement their own Volk-like function loader and tables of function pointers, then have the overloads call those. Which pretty much means the only way I can see to get non-linking to vulkan-1.lib to work with overloads is to have the headers implement all the gubbins of volk, which is a big change.

@pdaniell-nv
Copy link
Copy Markdown
Author

If these overloads require linking, them I'm not likely to use them nor advocate for them as one of the benefits of not linking is the ability to get the driver's functions and call those directly (or whatever the first layer in the chain is).

Vulkan-hpp has many examples of overloads without requiring linking, this is because they implement their own Volk-like function loader and tables of function pointers, then have the overloads call those. Which pretty much means the only way I can see to get non-linking to vulkan-1.lib to work with overloads is to have the headers implement all the gubbins of volk, which is a big change.

Could we move this discussion about overload functions to the other PR #2671, since this PR is about structure defaulting.

All the overloads are header-only inline functions and don't require linking.

@charles-lunarg
Copy link
Copy Markdown

Ah yes, my bad. Will continue discussion there.

@M2-TE
Copy link
Copy Markdown
Contributor

M2-TE commented Feb 6, 2026

Does this not re-implement what is already being offered in Vulkan-Hpp?

Of course, it is more lean, but as already mentioned above, the Vulkan C headers would then be enabling C++ features. This might end up clashing with or creating duplicate work alongside Vulkan-Hpp in the future, while also blurring the lines of what each set of headers is designed for (C vs C++).

@pdaniell-nv
Copy link
Copy Markdown
Author

Does this not re-implement what is already being offered in Vulkan-Hpp?

Of course, it is more lean, but as already mentioned above, the Vulkan C headers would then be enabling C++ features. This might end up clashing with or creating duplicate work alongside Vulkan-Hpp in the future, while also blurring the lines of what each set of headers is designed for (C vs C++).

Yes there is overlap between what this is doing and what is already in Vulkan-Hpp. The key difference is that the defaulting done by this PR affects the standard Vulkan API structs like VkPipelineRasterizationStateCreateInfo, and doesn't require the app to use something different like vk::PipelineRasterizationStateCreateInfo and its enums like vk::PolygonMode::eFill. I'm not saying Vulkan-Hpp is bad, what it has done is great, I love it, and developers can embrace Vulkan-Hpp if it works for them. This is an alternative for more traditional C/C++-lite developers as a seamless choice to incrementally improve their quality of life. Replacing Vulkan-Hpp is not a goal of this work. There are already some __cplusplus things in the standard headers for type safety.

@M2-TE
Copy link
Copy Markdown
Contributor

M2-TE commented Feb 7, 2026

Vulkan-Hpp structs/enums are binary compatible with the original Vulkan C structs. You can just cast between them if need be. This allows a very easy step-by-step transition to C++ if people want that, bringing all the advantages of that language along with it.

The __cplusplus is only used to export functions as C symbols without C++ name mangling. There are no specific C++ features in the Vulkan C headers yet afaik.

What exactly is this trying to achieve that Vulkan-Hpp does not already provide? Don't get me wrong, I like the features you propose here, it is partially why I use Vulkan-Hpp personally. I just feel like this is trying to shoehorn C++ idioms into C headers.

@SaschaWillems
Copy link
Copy Markdown
Contributor

SaschaWillems commented Feb 7, 2026

It'll most probably boil down to personal preferences. As for me, I don't like vulkan-hpp that much (modern C++, long build times), and I prefer the C headers. Having a C header that sits somewhere in between vulkan.h and vulkan.hpp would be something I'd personally use. That's also constant feedback from the community asking for something like this.

As this is going to be opt-in (as far as I understand) I don't see any issues with this. If people want a bit of C++ with the C headers to make things easier, they opt in. If they don't they use th C style. If they want more, they use vulkan.hpp. With that all bases are covered.

@M2-TE
Copy link
Copy Markdown
Contributor

M2-TE commented Feb 7, 2026

The complaints about long build times I can totally understand. That's definetely something that needs improvement.
I figured the use-case of having something between vulkan.h and vulkan.hpp was achieved using VULKAN_HPP_DISABLE_ENHANCED_MODE, with which you really only get namespaces, scoped enums and default-init structs.

Though, if there's constant community feedback about asking for this, then it is likely best to have it in vulkan.h too! Perhaps it can be used to simplify the Vulkan-Hpp side later on as well.

@M2-TE
Copy link
Copy Markdown
Contributor

M2-TE commented Feb 7, 2026

That aside, @pdaniell-nv @charles-lunarg since it is part of the discussion, what are your thoughts on having opinionated defaults for the struct members in the first place? Such as setting depthBiasSlopeFactor = 1.0f, which is part of this PR.

It is easy to know what to expect when everything is zero-initialized, but those defaults could muddy the waters a little. Could easily be a breaking change to change them later down the line too.

@martty
Copy link
Copy Markdown
Contributor

martty commented Feb 7, 2026

It'll most probably boil down to personal preferences. As for me, I don't like vulkan-hpp that much (modern C++, long build times), and I prefer the C headers. Having a C header that sits somewhere in between vulkan.h and vulkan.hpp would be something I'd personally use. That's also constant feedback from the community asking for something like this.

As this is going to be opt-in (as far as I understand) I don't see any issues with this. If people want a bit of C++ with the C headers to make things easier, they opt in. If they don't they use th C style. If they want more, they use vulkan.hpp. With that all bases are covered.

In my opinion, providing vk.xml is the method for catering to a continuum of preferences. It is unclear to me why it is useful to pick a specific interval on this c-style - modern cpp scale by the official C binding when this mechanism exists.

@zeux
Copy link
Copy Markdown

zeux commented Feb 9, 2026

My understanding is that to maintain compatibility with existing code, this can’t be enabled automatically under C++11 rules. Initializing members would break code like this that was previously valid (and idiomatic):

VkFenceCreateInfo createInfo = { VK_STRUCTURE_TYPE_FENCE_CREATE_INFO };

I believe this is fixed in C++14 and the new definitions would work with existing initialization like this there.

@fknfilewalker
Copy link
Copy Markdown

fknfilewalker commented Feb 10, 2026

I guess for c, the best is to just generate your own helpers from the xml. I once did this by creating two defines for each vulkan struct. It looked like this.

#ifdef VK_VERSION_1_1
#define CRVkBindBufferMemoryInfo(...) { VK_STRUCTURE_TYPE_BIND_BUFFER_MEMORY_INFO, ##__VA_ARGS__ }
#define CLVkBindBufferMemoryInfo(name,...) VkBindBufferMemoryInfo name = { VK_STRUCTURE_TYPE_BIND_BUFFER_MEMORY_INFO, ##__VA_ARGS__ }

Eg for VkBindBufferMemoryInfo, one could use a prefix of CR for an rvalue initialization and CL for lvalue. I mostly created this in order to remove stypes from my bug list but also because stypes are quite annoying to type.

// init as rvalue
VkBindBufferMemoryInfo info = CRVkBindBufferMemoryInfo(NULL, buffer,...);
// init as lvalue with name info
CRVkBindBufferMemoryInfo(info, NULL, buffer,...);
info.memoryOffset - 0;

Extending this with different combinations of prefixes to set pNext always to NULL would have also been possible

For pNext chaining I also created a define that allowed for

cvkChain(head, middle, ..., tail)

which was autogenerated up to a certain count

#define CVK_EXPAND(x) x 
#define CVK_ARG_N(_1,_2,_3,_4,_5,_6,_7,_8,_9,_10,_11,_12,_13,_14,_15,_16,N,...) N 
#define CVK_NARG(...) CVK_EXPAND(CVK_ARG_N(__VA_ARGS__,16,15,14,13,12,11,10,9,8,7,6,5,4,3,2,1,0))

#define CVK_CHAIN_1_(V) CVK_CHAIN_1 V
#define CVK_CHAIN_2_(V) CVK_CHAIN_2 V
#define CVK_CHAIN_3_(V) CVK_CHAIN_3 V
#define CVK_CHAIN_4_(V) CVK_CHAIN_4 V

#define CVK_CHAIN_2(A,B) A.pNext = &B
#define CVK_CHAIN_3(_0,_1,_2) CVK_CHAIN_2(_0,_1); CVK_CHAIN_2(_1,_2)
#define CVK_CHAIN_4(_0,_1,_2,_3) CVK_CHAIN_3(_0,_1,_2); CVK_CHAIN_2(_2,_3)
#define CVK_CHAIN_5(_0,_1,_2,_3,_4) CVK_CHAIN_4(_0,_1,_2,_3); CVK_CHAIN_2(_3,_4)

#define CVK_CHAIN__(N, ...)		CVK_CHAIN_ ## N ## _((__VA_ARGS__))
#define CVK_CHAIN_(N, ...)		CVK_CHAIN__(N, __VA_ARGS__)
#define cvkChain(...)			CVK_CHAIN_(CVK_NARG(__VA_ARGS__), __VA_ARGS__)

@krOoze
Copy link
Copy Markdown
Contributor

krOoze commented Feb 12, 2026

There's also https://github.com/KhronosGroup/Vulkan-Utility-Libraries/blob/main/include/vulkan/utility/vk_struct_helper.hpp

Arguably this mixes constants and defaults. sType is a true constant\invariant. Meanwhile pNext is supposed to be set to desired value, of which nullptr is only the most common. If defaults are also to be part of the headers, there are far more defaultable parameters than just pNext.

@SaschaWillems
Copy link
Copy Markdown
Contributor

Did a version of my "How to Vulkan in 2026" tutorial with this and really liking this. It removes a lot of verbosity and clearly improves code readability. Esp. with C++ requiring members for designated initializers to be in order, which usually means sType has to go first.

@SaschaWillems
Copy link
Copy Markdown
Contributor

Any preferred way of listing possible additional sane defaults that could make sense?

E.g. VkSwapchainCreateInfoKHR::imageArrayLayers will probably be 1 in most of the cases.

@pdaniell-nv
Copy link
Copy Markdown
Author

Any preferred way of listing possible additional sane defaults that could make sense?

E.g. VkSwapchainCreateInfoKHR::imageArrayLayers will probably be 1 in most of the cases.

Over the next few days I'm going to do a pass on the vk.xml file to add a ton more default= attributes. I'm sure we'll iterate over this for a little while to strike a good balance between convenience and being opionated.

@gpx1000
Copy link
Copy Markdown
Contributor

gpx1000 commented Feb 17, 2026

To the Vulkan-HPP discussion, I don't think there's any actual competition between this and Vulkan-HPP. I think they serve two different audiences. In fact, I think Vulkan-HPP might be able to offer this as a natural extension point into the rest of Vulkan-HPP should people want to take advantage of more modern C++ practices which many will.

That type of architecture might even benefit Vulkan-HPP while we wait for modules to exist to have a more ala carte' style c++ inclusion based upon needs which will drive down compile time. Keeping this focused on a specific need / use case can only serve as a good benefit for the Vulkan community.

This is a great way to make Vulkan easier to use for new developers. I haven't commented before now because the most I really have to say is +1. Great effort and I love where this is going well done!

@M2-TE
Copy link
Copy Markdown
Contributor

M2-TE commented Feb 17, 2026

Agreed, this will likely directly benefit Vulkan-Hpp too. Merely gotta be careful with the opinionated defaults imo.

That type of architecture might even benefit Vulkan-HPP while we wait for modules to exist to have a more ala carte' style c++ inclusion based upon needs which will drive down compile time

Could you expand on what you mean by that? We are actively developing the module with plans to drop the experimental tag soon, so it would be nice to explore all the options beforehand. An issue over there would be very appreciated! (as to not clog this PR)

@ShabbyX
Copy link
Copy Markdown
Contributor

ShabbyX commented Feb 19, 2026

I just noticed an issue with the way the header decides if defaults should be used or not. For reference, I'm talking about this:

#ifndef VK_CPP11_DEFAULT
    #if (defined(__cplusplus) && (__cplusplus >= 201103L)) || (defined(_MSVC_LANG) && (_MSVC_LANG >= 201103L))
        #define VK_CPP11_DEFAULT(value) { value }
    #else
        #define  VK_CPP11_DEFAULT(value)
    #endif
#endif

If the default is setting the only valid value, there's nothing wrong with that. But if it's setting something for convenience, then it must be opt-in. Let's take VkPipelineColorBlendAttachmentState::colorWriteMask and say we default it to 0xF.

I can have an app today that knowingly does this (contrived, but for the sake of example):

// Note: this sets everything to 0
VkPipelineColorBlendAttachmentState attachment = {};
if (enable_r) attachment.colorWriteMask |= VK_COLOR_COMPONENT_R_BIT;
if (enable_g) attachment.colorWriteMask |= VK_COLOR_COMPONENT_G_BIT;
if (enable_b) attachment.colorWriteMask |= VK_COLOR_COMPONENT_B_BIT;
if (enable_a) attachment.colorWriteMask |= VK_COLOR_COMPONENT_A_BIT;

If we simply default colorWriteMask to 0xF by virtue of compiling in C++, suddenly my app is broken.

@zeux
Copy link
Copy Markdown

zeux commented Feb 19, 2026

This is a great point; I think for non-bitmasks, changing the default is only safe if 0 was an invalid value previously.

E.g. sType defaulting is safe and correct. Defaulting every field, if appropriate, to 0 is safe (modulo my C++14 comment above - this entire change is breaking in C++11).

Defaulting lineWidth to 1.0f is a little borderline; 0 is only valid if wideLines is set and the range starts at 0, at which point using 0 line width has questionable utility - so I think it's safe in practice, but technically potentially breaking.

VkImageCreateInfo::mipLevels/arrayLevels are invalid when set to 0 so defaulting to a different value is safe. VkImageCreateInfo::sampleCount is a little strange: it's sort of a bitmask so I could see code that sets individual bits using |, but that probably never happens in practice...

VkViewport::maxDepth is valid as 0, and there might be valid reasons to leave it as such (e.g. minDepth can be 1 to flip the range). Unsure if this one is safe in practice.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Request for Comment Requests for community feedback

Projects

None yet

Development

Successfully merging this pull request may close these issues.