Structure Chaining
Introduction
The Structure Chaining methodology logically sits between the Managed Chaining methodology and the Raw Chaining methodology. In a very real sense, it can either be seen as having the best elements of both, or of being a compromise that isn't as good as either alternative!
Notwithstanding, the methodology makes use of a set of well documented extension methods, found
in ChainExtensions
to allow the type-safe manipulation
of IChainable
structures efficiently; with the exception of the ref Chain(out)
instance convenience method which we
cover below, that is implemented automatically on any structure that implements IChainStart
.
Usage
Creation (Chain)
You can happily create the start of a chain as you would with Raw Chaining, by declaring a variable
of the chain's head first. Indeed it is necessary to do so if you wish to specify non-default values for the head
structure (though you can also make use of SetNext
s replace functionality). You also need to use this approach when
starting a chain which is not explicitly defined as a chain start by the specification. If you
do start a chain with such a structure, you will have to use the *Any
overloads of the extension
methods below to continue manipulating it.
Regardless, the SType
and PNext
will be overwritten whenever you start manipulating the chain, so you should never
set them manually. For example:
var createInfo = new DeviceCreateInfo
{
Flags = 1U
};
// When you call any chaining method it will set the chain's SType automatically.
createinfo.AddNext...
In many cases, we only want to create a default structure for population by the API. To do so, we can make use of the
static ref Chain(out)
convenience method like so:
PhysicalDeviceFeatures2.Chain(out var features2)
This has several advantages:
- The method is only available for structures that are valid at the start of a chain; providing compile-time validation.
- The structure's
SType
will be correctly set immediately. - The syntax is fluent, and creates more readable code when used with the other chaining methods ( see below).
Understanding the Chain method's return value
All the chaining methods return the current start of the chain by reference (including ref Chain(out)
). This allows
each method to scan the entire chain. More importantly, it allows the Type constraints to be checked during compile time
to ensure that a type actually can extend the chain (unless you are using the *Any
overloads which
define looser constraints). One side effect of this approach is that ref Chain(out)
outputs the newly created chain _
and_ returns a reference to it. This can cause confusion to less experienced C# devs, for example:
// TL;DR Never do this assignment
var a = ChainStart.Chain(out var b).AddNext(out ChainExtension c);
Both a
and b
will appear to be identical structures; however 'a' is actually a copy of 'b', being separate
locations on the current stack frame. In most cases, that is really no problem at all (though there is a performance
impact due to the copy) as the copy occurs at the end of the statement (during the assignment operation) so both a
and b
are a copy of the start of the chain that point to the same next item. However, if further modifications are
made to the head of either a
or b
, they will not be reflected in the other. None of this is undefined behaviour, but
as it is generally poorly understood none of the examples ever recommend assigning the output of a chain, and it should
be avoided.
The ref Chain(out)
method is a static method that is only implemented automatically on IChainStart
structures, the
remaining methods are actually extension methods.
'Any' Overloads
The methods ending with Any
can be used with any IChainable
structure, but they do not constrain the entries, or the
head of the chain to being structures explicitly mentioned by the specification. The non-Any
methods are more
restrictive, and should usually be used in preference.
Extending the chain
Adding (AddNext/AddNextAny)
The most common use case is to add an empty structure to the end of a chain for it to be populated by the Vulkan API,
this can be done using the AddNext
method like so:
PhysicalDeviceFeatures2
.Chain(out var features2)
// CreateNext will create an empty struct, with the correct SType (as well as ensuring the
// chain's SType is set correctly).
.AddNext(out PhysicalDeviceDescriptorIndexingFeatures indexingFeatures)
.AddNext(out PhysicalDeviceAccelerationStructureFeaturesKHR accelerationStructureFeaturesKhr);
Each method out
puts a struct into the local stack frame for querying once populated, and the pointers point to this
local variable. Despite generics and interfaces being used, the chain methods avoid the heap entirely.
Try adding (TryAddNext/TryAddNextAny)
You may only want to add a structure if it doesn't already exist in the chain, this can be done with TryAddNext
, e.g.:
PhysicalDeviceFeatures2
.Chain(out var features2)
.AddNext(out PhysicalDeviceDescriptorIndexingFeatures indexingFeatures)
// As there is already a PhysicalDeviceDescriptorIndexingFeatures structure the following
// will not add anything to the chain and `added` will be `false`.
.TryAddNext(out PhysicalDeviceDescriptorIndexingFeatures indexingFeatures2, out bool added);
Setting or Replacing (SetNext/SetNextAny)
Sometimes we may wish to set the initial state of a structure, or replace any existing item within the structure that
has the same StructureType
we can do this with SetNext
:
var indexingFeatures = new PhysicalDeviceDescriptorIndexingFeatures
{
ShaderInputAttachmentArrayDynamicIndexing = true
};
// Unlike AddNext, SetNext will only add the structure if isn't already present, otherwise it will overwrite it.
// So we can provide a default to SetNext like so:
var accelerationStructureFeaturesKhr = default(PhysicalDeviceAccelerationStructureFeaturesKHR);
PhysicalDeviceFeatures2
.Chain(out var features2)
// SetNext accepts an existing struct, note, it will coerce the SType and blank the PNext
.SetNext(ref indexingFeatures)
.SetNext(ref default(PhysicalDeviceAccelerationStructureFeaturesKHR));
NOTE you can mix and match AddNext
and SetNext
(and any chaining method) in the same method chain.
By default, SetNext
will replace any item in the chain with a matching SType
, this behaviour can be changed by
setting the optional alwaysAdd
parameter to true
;
var indexingFeatures = new PhysicalDeviceDescriptorIndexingFeatures
{
ShaderInputAttachmentArrayDynamicIndexing = true
};
var indexingFeatures2 = new PhysicalDeviceDescriptorIndexingFeatures
{
ShaderInputAttachmentArrayDynamicIndexing = false
};
var accelerationStructureFeaturesKhr = new PhysicalDeviceAccelerationStructureFeaturesKHR
{
AccelerationStructure = true
};
PhysicalDeviceFeatures2
.Chain(out var features2)
.SetNext(ref indexingFeatures)
// This will add the second 'indexingFeatures' struct, even though one is already present in the chain.
.SetNext(ref indexingFeatures2, true);
Indexing (IndexOf/IndexOfAny)
Sometimes it's useful to know if a structure you previously supplied is still in a chain, this can be done
with IndexOf
, which returns a non-negative index (zero-indexed) if the structure is found, eg.:
PhysicalDeviceFeatures2
.Chain(out var features2)
.AddNext(out PhysicalDeviceDescriptorIndexingFeatures indexingFeatures)
.AddNext(out PhysicalDeviceAccelerationStructureFeaturesKHR accelerationStructureFeaturesKhr);
// Check indices
Assert.Equal(1, features2.IndexOf(ref indexingFeatures));
Assert.Equal(2, features2.IndexOf(ref accelerationStructureFeaturesKhr));
Performance
Structure chaining has the advantage that it avoids the heap entirely, and encourages you to pass the structures around as references (avoiding copies). Note though, it can't stop you from sticking something on the heap or doing an unnecessary copy, but it doesn't encourage the behaviour.
However, every action begins at the chain's head, so extending the chain is always an O(n) operation. Technically, it
should still be faster than extending a Managed Chain which needs to
copy the entire chain so is also O(n), as it merely skips from pointer to pointer, whilst coercing the SType
field.
However Raw Chaining can extend a chain quickly if it keeps track of the tail.
The pay off is a compact syntax that works well with IDE tools and compile time checking, due to the type constraint system, whilst encouraging good practice. As such, it should be compared to Raw Chaining when trying to optimise a hot path.