diff --git a/Silk.NET.sln b/Silk.NET.sln
index 78bccfff41..92d599de4e 100644
--- a/Silk.NET.sln
+++ b/Silk.NET.sln
@@ -270,9 +270,9 @@ EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Silk.NET.NUKE", "build\nuke\Silk.NET.NUKE.csproj", "{B9A8D738-FE7D-4860-A446-4A03E3DDEB74}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Microsoft", "Microsoft", "{F2CF5D32-4B41-425E-B229-8FFC48F88063}"
-ProjectSection(SolutionItems) = preProject
- src\Microsoft\dxva.h = src\Microsoft\dxva.h
-EndProjectSection
+ ProjectSection(SolutionItems) = preProject
+ src\Microsoft\dxva.h = src\Microsoft\dxva.h
+ EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Silk.NET.Direct3D11", "src\Microsoft\Silk.NET.Direct3D11\Silk.NET.Direct3D11.csproj", "{F3B7A9D6-5B15-45E8-925B-20B5BBD33428}"
EndProject
@@ -474,6 +474,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Silk.NET.OpenXR.Extensions.
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Silk.NET.OpenXR.Extensions.HTCX", "src\OpenXR\Extensions\Silk.NET.OpenXR.Extensions.HTCX\Silk.NET.OpenXR.Extensions.HTCX.csproj", "{782B6A7E-9F04-429A-9DCD-D7273AA3882E}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "PrototypeStructChaining", "PrototypeStructChaining", "{B15922CB-815C-4038-B635-EE2D8A8F700B}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PrototypeStructChaining", "src\Lab\Experiments\PrototypeStructChaining\PrototypeStructChaining\PrototypeStructChaining.csproj", "{EEFF37DA-E4F2-406E-AF97-8615BB7BC34C}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PrototypeStructChaining.Test", "src\Lab\Experiments\PrototypeStructChaining\PrototypeStructChaining.Test\PrototypeStructChaining.Test.csproj", "{BD19250B-E143-4F4E-9E1D-18829CCB3642}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -2823,6 +2829,30 @@ Global
{782B6A7E-9F04-429A-9DCD-D7273AA3882E}.Release|x64.Build.0 = Release|Any CPU
{782B6A7E-9F04-429A-9DCD-D7273AA3882E}.Release|x86.ActiveCfg = Release|Any CPU
{782B6A7E-9F04-429A-9DCD-D7273AA3882E}.Release|x86.Build.0 = Release|Any CPU
+ {EEFF37DA-E4F2-406E-AF97-8615BB7BC34C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {EEFF37DA-E4F2-406E-AF97-8615BB7BC34C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {EEFF37DA-E4F2-406E-AF97-8615BB7BC34C}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {EEFF37DA-E4F2-406E-AF97-8615BB7BC34C}.Debug|x64.Build.0 = Debug|Any CPU
+ {EEFF37DA-E4F2-406E-AF97-8615BB7BC34C}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {EEFF37DA-E4F2-406E-AF97-8615BB7BC34C}.Debug|x86.Build.0 = Debug|Any CPU
+ {EEFF37DA-E4F2-406E-AF97-8615BB7BC34C}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {EEFF37DA-E4F2-406E-AF97-8615BB7BC34C}.Release|Any CPU.Build.0 = Release|Any CPU
+ {EEFF37DA-E4F2-406E-AF97-8615BB7BC34C}.Release|x64.ActiveCfg = Release|Any CPU
+ {EEFF37DA-E4F2-406E-AF97-8615BB7BC34C}.Release|x64.Build.0 = Release|Any CPU
+ {EEFF37DA-E4F2-406E-AF97-8615BB7BC34C}.Release|x86.ActiveCfg = Release|Any CPU
+ {EEFF37DA-E4F2-406E-AF97-8615BB7BC34C}.Release|x86.Build.0 = Release|Any CPU
+ {BD19250B-E143-4F4E-9E1D-18829CCB3642}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {BD19250B-E143-4F4E-9E1D-18829CCB3642}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {BD19250B-E143-4F4E-9E1D-18829CCB3642}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {BD19250B-E143-4F4E-9E1D-18829CCB3642}.Debug|x64.Build.0 = Debug|Any CPU
+ {BD19250B-E143-4F4E-9E1D-18829CCB3642}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {BD19250B-E143-4F4E-9E1D-18829CCB3642}.Debug|x86.Build.0 = Debug|Any CPU
+ {BD19250B-E143-4F4E-9E1D-18829CCB3642}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {BD19250B-E143-4F4E-9E1D-18829CCB3642}.Release|Any CPU.Build.0 = Release|Any CPU
+ {BD19250B-E143-4F4E-9E1D-18829CCB3642}.Release|x64.ActiveCfg = Release|Any CPU
+ {BD19250B-E143-4F4E-9E1D-18829CCB3642}.Release|x64.Build.0 = Release|Any CPU
+ {BD19250B-E143-4F4E-9E1D-18829CCB3642}.Release|x86.ActiveCfg = Release|Any CPU
+ {BD19250B-E143-4F4E-9E1D-18829CCB3642}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -3052,6 +3082,9 @@ Global
{3E30D674-9282-4297-AD1F-9B233A166308} = {0651C5EF-50AA-4598-8D9C-8F210ADD8490}
{606214B8-07FC-436F-9523-02AF32E1AB1E} = {90471225-AC23-424E-B62E-F6EC4C6ECAC0}
{782B6A7E-9F04-429A-9DCD-D7273AA3882E} = {90471225-AC23-424E-B62E-F6EC4C6ECAC0}
+ {B15922CB-815C-4038-B635-EE2D8A8F700B} = {39B598E9-44BA-4A61-A1BB-7C543734DBA6}
+ {EEFF37DA-E4F2-406E-AF97-8615BB7BC34C} = {B15922CB-815C-4038-B635-EE2D8A8F700B}
+ {BD19250B-E143-4F4E-9E1D-18829CCB3642} = {B15922CB-815C-4038-B635-EE2D8A8F700B}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {F5273D7F-3334-48DF-94E3-41AE6816CD4D}
diff --git a/build/cache/vulkan.json.gz b/build/cache/vulkan.json.gz
index 98d27c384c..5a4565b913 100644
Binary files a/build/cache/vulkan.json.gz and b/build/cache/vulkan.json.gz differ
diff --git a/documentation/proposals/Proposal - Vulkan Struct Chaining - #1 StructureType correction.md b/documentation/proposals/Proposal - Vulkan Struct Chaining - #1 StructureType correction.md
new file mode 100644
index 0000000000..092e78bbf5
--- /dev/null
+++ b/documentation/proposals/Proposal - Vulkan Struct Chaining - #1 StructureType correction.md
@@ -0,0 +1,92 @@
+# Summary
+
+This proposal is a minimal enhancement to [`Silk.Net.Vulkan`](../../src/Vulkan/Silk.NET.Vulkan) to mark all structures
+that contain a `StructureType SType` field as implementing the interface `IStructuredType`.
+
+This is a tiny pre-requisite for
+[Proposal - Vulkan Struct Chaining - #2 Unmanaged Chaining.md](Proposal%20-%20Vulkan%20Struct%20Chaining%20-%20%232%20Unmanaged%20Chaining.md)
+and is of limited value otherwise. Its primary purpose is to mark any structure that requires it's `SType` field to be
+correctly set when passing to Vulkan, and to provide a mechanism for doing so.
+
+A full implementation can be found [in Pull Request 680](https://github.com/dotnet/Silk.NET/pull/680).
+
+# Contributors
+
+- [Craig Dean, DevDecoder](https://github.com/thargy)
+
+# Current Status
+
+- [x] Proposed
+- [ ] Discussed with API Review Board (ARB)
+- [ ] Approved
+- [x] Implemented
+
+# Design Decisions
+
+- The `IStructuredType` interface will usually not be implemented directly, instead `IChainable` (from
+ the [unmanaged chaining proposal](Proposal%20-%20Vulkan%20Struct%20Chaining%20-%20%232%20Unmanaged%20Chaining.md))
+ will extend this interface.
+- Whenever the `IStructuredType` is added to an interface (either directly or indirectly) the
+ corresponding `StructureType()` method should also be explicitly
+ implemented ([see below](#istructuretype-implementation)).
+- This proposed interface could easily be combined/merged with the `IChainable`
+ interface which [is proposed](Proposal%20-%20Vulkan%20Struct%20Chaining%20-%20%232%20Unmanaged%20Chaining.md) as an
+ extension. However, that interface marks a struct as having a second field `void* PNext`, as well as requiring
+ that `StructureType sType` field is in the first position, which is not required by this proposal (but is allowed).
+ Keeping the two concepts separate is good encapsulation and good for supporting future changes. The concept
+ that an `SType` must be correct is somewhat different to the concept of a chain (implied by `void* PNext`).
+- To be clear, this proposal does not need to guarantee that the `SType` field is in position 0 (i.e. first),
+ that requirement is only necessary to implement the functionality
+ [proposed by the unmanaged chaining system](Proposal%20-%20Vulkan%20Struct%20Chaining%20-%20%232%20Unmanaged%20Chaining.md)
+- Not all structures exposed by Vulkan have a default value for the `SType`, (e.g. `BaseOutStructure` and `BaseInStructure`),
+ where no default is available, the `StructureType()` method only returns the current value, rather than setting it.
+
+# Implementation Notes
+
+- BuildTools already contains enough information to determine whether the interface should be added to a structure, and
+ already knows which `StructureType` the structure should use.
+- A working example of such a struct can
+ be [found in the labs.](../../src/Lab/Experiments/PrototypeStructChaining/PrototypeStructChaining/PhysicalDeviceFeatures2.cs)
+
+# Proposed API
+
+## StructureType structures
+
+### IStructuredType
+
+This proposal adds a single interface `IStructuredType`, the primary purpose of which is to mark any structure that
+requires it's `SType` to be correctly set when passing to Vulkan. It adds a single
+method `void IStructuredType.StructureType()`
+which sets the `SType` correctly and returns it to the caller.
+
+```csharp
+namespace Silk.Net.Vulkan;
+
+///
+/// Base interface for any struct that has a field called `SType`, that must be correctly
+/// set when passing into the Vulkan API.
+///
+public interface IStructuredType
+{
+ ///
+ /// Gets the structured type's enum value.
+ ///
+ ///
+ /// Retrieving the also ensures it is set to the correct value (if any).
+ ///
+ StructureType StructureType();
+}
+```
+
+### IStructureType implementation
+
+Each struct generated that implements `IStructuredType` should also have the following code auto-generated, to
+explicitly implement the interface. The method sets and returns the `SType` correctly (if any) for the current structure.
+
+```csharp
+///
+StructureType IStructuredType.StructureType()
+{
+ return SType = StructureType.;
+}
+```
diff --git a/documentation/proposals/Proposal - Vulkan Struct Chaining - #2 Unmanaged Chaining.md b/documentation/proposals/Proposal - Vulkan Struct Chaining - #2 Unmanaged Chaining.md
new file mode 100644
index 0000000000..35f13a7d9b
--- /dev/null
+++ b/documentation/proposals/Proposal - Vulkan Struct Chaining - #2 Unmanaged Chaining.md
@@ -0,0 +1,735 @@
+# Summary
+
+**_This proposal is dependent
+on [Proposal - Vulkan Struct Chaining - #1 StructureType correction](Proposal%20-%20Vulkan%20Struct%20Chaining%20-%20%231%20StructureType%20correction.md)
+._**
+
+This proposal presents a lightweight mechanism for fluently building Vulkan Structure Chains. You may wish to start with
+the [Usage section below](#Usage) to aid understanding. There is also a fully working prototype
+[in Pull Request 683](https://github.com/dotnet/Silk.NET/pull/683).
+
+To do so it marks any structure that meets the following requirements as being `IChainable`:
+
+* Is a struct.
+* Has a `StructureType SType` field in position 0 (first field)
+* Has a `void* PNext` field in position 1 (second field)
+
+The `IChainable` interface extends the `IStructuredType` interface
+from [Proposal - Vulkan Struct Chaining - #1 StructureType correction](Proposal%20-%20Vulkan%20Struct%20Chaining%20-%20%231%20StructureType%20correction.md)
+and so the explicit implementation of `IChainable.StructureType()` from
+[that proposal](Proposal%20-%20Vulkan%20Struct%20Chaining%20-%20%231%20StructureType%20correction.md#istructuretype-implementation)
+is triggerred for the structure, providing a mechanism for ensuring the `SType` is correctly set. It then exposes
+a `BaseInStructure* PNext { get; set; }` property for easy access to the next item in the chain.
+
+The presence of the `IChainable` interface, also acts as a **guarantee** that it is safe to cast any pointer of a struct
+implementing it to a pointer to a `BaseInStructure` struct, which is a struct which has just the `SType` and `PNext`
+fields present. Therefore it is always possible to cast `void* PNext` of an `IChainable` struct to `BaseInStructure*`.
+It is this guarantee that requires the position of the fields to be fixed (which they are in practice). However, by
+ensuring we validate the constraints at build time (when choosing to add the interface), we can prevent downstream bugs
+occurring at run time.
+
+**Note** that the `IChainable` interface adds the additional constraint that the `StructureType SType` field must be at
+offset 0, i.e. in the first position to facilitate this functionality - which is not a constraint of `IStructuredType`.
+
+However, rather than extending `IChainable` directly, where
+the [Vulkan Specification](https://raw.githubusercontent.com/KhronosGroup/Vulkan-Headers/master/registry/vk.xml))
+specifies chaining constraints, via the presence of the `structextends` attribute, `BuildTools` chooses one
+of `IChainStart` or `IExtendsChain` (both of which extend `IChainable`). The specification has nearly 100 chains
+defined in this manner, and many of the >700 structures form part of these chains. However, over 200 structures are not
+curretnly defined as part of a chain, and as such, all the utilities proposed have a 'looser' mechanism for working
+with `IChainable` directly when it is necessary to do so.
+
+For example, if `struct B` extends `struct A`, then `struct B` will be marked with `IExtendsChain` and `struct A`
+will be marked with `IChainStart`. A struct may only extend `IChainStart` once (even though it may appear in
+the `structextends` attribute of many structs), but is may implement multiple `IExtendsChain` interfaces. It is
+also feasible for a struct to implement both (i.e. be able to extend other chains, as well as being a chain start), a
+real example of this can be seen
+[in the labs](../../src/Lab/Experiments/PrototypeStructChaining/PrototypeStructChaining/PhysicalDeviceFeatures2.cs).
+
+As a result, `IChainable` will not usually be directly implemented (just as it is unlikely to see `IStructuredType`
+[implemented directly]((Proposal%20-%20Vulkan%20Struct%20Chaining%20-%20%231%20StructureType%20correction.md))).
+However, if
+the [Vulkan Specification](https://raw.githubusercontent.com/KhronosGroup/Vulkan-Headers/master/registry/vk.xml) ever
+includes a struct that meets the above constraints, but doesn't have, or appear in, a `structsextend` attribute, then it
+will be explicitly marked as `IChainable`.
+
+Whenever a struct is marked as `IChainStart` a static `ref [StructType] Chain(out [StructType]) capture);` method is
+also added, providing an easy form of starting a chain with default values. As `IChainStart` also
+implements `IChainable`, which implements `IStructuredType`, then a chain start will have three additional methods
+generated (the static `Chain(out)` method and the explicit `IStructuredType.StructureType()` and `IChainable.PNext`
+implementation); as compared to all other `IChainable` structs, which will only have the
+explicit `IStructuredType.StructureType()` implementation from
+[Proposal - Vulkan Struct Chaining - #1 StructureType correction](Proposal%20-%20Vulkan%20Struct%20Chaining%20-%20%231%20StructureType%20correction.md)
+and the explicit `BaseInStructure* IChainable.PNext { get; set;}` property from this proposal.
+
+The remaining functionality is provided entirely by the following new extension methods:
+
+```csharp
+public static unsafe ref TChain SetNext(this ref TChain chain, ref TNext value,
+ bool alwaysAdd = false)
+ where TChain : struct, IChainStart
+ where TNext : struct, IExtendsChain {...}
+
+public static unsafe ref TChain SetNextAny(this ref TChain chain, ref TNext value,
+ where TChain : struct, IChainable
+ where TNext : struct, IChainable {...}
+
+public static unsafe ref TChain AddNext(this ref TChain chain, out TNext next)
+ where TChain : struct, IChainStart
+ where TNext : struct, IExtendsChain {...}
+
+public static unsafe ref TChain AddNextAny(this ref TChain chain, out TNext next)
+ where TChain : struct, IChainable
+ where TNext : struct, IChainable {...}
+
+public static unsafe ref TChain TryAddNext(this ref TChain chain, out TNext next, out bool added)
+ where TChain : struct, IChainStart
+ where TNext : struct, IExtendsChain {...}
+
+public static unsafe ref TChain TryAddNextAny(this ref TChain chain, out TNext next, out bool added)
+ where TChain : struct, IChainable
+ where TNext : struct, IChainable {...}
+
+public static unsafe int IndexOf(this ref TChain chain, ref TNext value)
+ where TChain : struct, IChainStart
+ where TNext : struct, IExtendsChain {...}
+
+public static unsafe int IndexOfAny(this ref TChain chain, ref TNext value)
+ where TChain : struct, IChainable
+ where TNext : struct, IChainable {...}
+```
+
+An implementation can be found [in Pull Request 683](https://github.com/dotnet/Silk.NET/pull/683) and their use is
+detailed below.
+
+# Contributors
+
+- [Craig Dean, DevDecoder](https://github.com/thargy)
+
+# Current Status
+
+- [x] Proposed
+- [ ] Discussed with API Review Board (ARB)
+- [ ] Approved
+- [x] Implemented
+
+# Design Decisions
+
+The following requirements are achieved by this proposal:
+
+- **Backward compatibility** - the chaining system should not modify the existing structs, but add functionality.
+- **Minimal bloat** - the minimum amount of new generated code was sought.
+- **Discoverability** - it should be as easy as possible for a new user to discover.
+- **Compile-time Validation** - it should prevent chaining invalid structures (as much as possible) during compilation.
+- **SType coercion** - it should always set the `SType` of chained structures.
+- **Pointer management** - it should handle setting the `PNext` pointer of structures.
+- **Compact usage** - it should reduce copy-pasta code.
+- **Avoids the heap** - boxing should be avoided.
+- **Well Tested** - tests were added to ensure pointers are correctly set, and compilation failures occur.
+
+What this proposal does not do (some of these _are_ addressed
+in [Proposal - Vulkan Struct Chaining - #3 Managed Chaining](Proposal%20-%20Vulkan%20Struct%20Chaining%20-%20%233%20Managed%20Chaining.md))
+is manage pointers of structures that find themselves on the heap. Any supplied structures should be held on the stack,
+once moved to the heap their `PNext` values can no longer be trusted as the GC is free to move structures in heap
+memory. The proposed extension methods make it difficult to use with heap objects, but it is not impossible. The
+presence of the `ManagedChain` classes
+from [the proposal for managed chains](Proposal%20-%20Vulkan%20Struct%20Chaining%20-%20%233%20Managed%20Chaining.md),
+along with a well documented API should highlight the danger of such practices.
+
+Indeed, it is important to remember that such dangers are already part of the existing implementation and are a feature
+of using unmanaged pointers in .NET rather than a limitation of this proposal.
+
+To be discussed:
+
+- Whether to include [specific chain interfaces](#chain-interfaces-optional).
+
+# Usage
+
+The proposal provides for the following usages. Note that where an `Any` extension method is mentioned, it is identical
+to the non-`Any` version (e.g. `AddNext` and `AddNextAny` are equivalent), save that the `Any` version does not
+constrain the types to those associated with a defined chain, instead types only need to be `IChainable`.
+
+### Chain Building
+
+You can happily create the start of a chain as usual, by declaring a variable first. Indeed it is necessary to do so if
+you wish to specify non-default values (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` versions 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:
+
+```csharp
+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 use the
+static `BaseInStructure` method like so:
+
+```csharp
+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).
+
+**Note** All the chaining methods return the current start of the chain by reference (including `BaseInStructure`). 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. One side effect 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:
+
+```csharp
+// Don't do this, it is harmless but unnecessary and confusing!
+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 as both point to the _start_ of a chain, and
+so there are no unmanaged pointers pointing _to them_, therefore using _either_ is fine (though completely unnecessary).
+
+However, for deeper understanding the chain extension methods actually _take a reference_ to `this`, so `AddNext`
+actually updates the `PNext` of variable `b`. Once the chain is built the final assignment _to_ `a` _copies_ `b`
+into `a`. None of this is undefined behaviour, but as it is generally poorly understood so none of the examples ever
+recommend assigning the output of a chain.
+
+The `Chain` method is a static method implemented on `IChainStart` structures, the remaining methods are actually
+extension methods. 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.
+
+### 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 now be done like so:
+
+```csharp
+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.
+
+### 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.:
+
+```csharp
+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);
+```
+
+### 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`:
+
+```csharp
+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`;
+
+```csharp
+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);
+```
+
+### 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.:
+
+```csharp
+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));
+```
+
+### Validation
+
+Due to the generic constraints, all the chain extensions are only valid on a struct that implements `IChainStart` and
+only accept struct that implement `IExtends` where `TChain` is the chain type. This means that it is impossible
+to add invalid structs to a chain with these methods. Further, using any of the chain extension methods guarantees that
+the chain, and the supplied item have the correct `SType`, **and** that the `PNext` pointers along the chain are valid.
+
+# Proposed API
+
+## Chaining interfaces
+
+### IChainable
+
+The base interface of chaining interfaces:
+
+```csharp
+namespace Silk.Net.Vulkan;
+
+///
+/// Base interface for any struct that has can set the next value.
+///
+/// Note that any structure marked must start with a
+/// and a void* field, in that order. This is so that a pointer to it can be coerced
+/// to a pointer to a .
+public interface IChainable : IStructuredType
+{
+ ///
+ /// Points to the next in this chain, if any; otherwise .
+ ///
+ unsafe BaseInStructure* PNext { get; set; }
+}
+
+```
+
+### IChainStart
+
+Implemented by any struct that can be used as the start of a chain:
+
+```csharp
+namespace Silk.Net.Vulkan;
+
+///
+/// Marks a chainable struct as being allowed at the start of a chain.
+///
+/// Any will have a corresponding static `Chain(out var chain)`
+/// convenience method.
+public interface IChainStart : IChainable
+{
+}
+```
+
+### IExtendsChain
+
+Implemented by any struct that can be added to a chain.
+
+```csharp
+namespace Silk.Net.Vulkan;
+
+///
+/// Marks a chainable struct indicating which chain this type
+/// extends.
+///
+/// A chain start structure.
+public interface IExtendsChain : IChainable
+ where TChain : IChainable
+{
+}
+```
+
+### Chain Extension Methods
+
+Provides the struct chaining functionality, the full implementation can be
+found [in the labs](../../src/Lab/Experiments/PrototypeStructChaining/PrototypeStructChaining/ChainExtensions.cs):
+
+```csharp
+namespace Silk.NET.Vulkan;
+
+///
+/// Extension methods and utilities for building unmanaged structure chains.
+///
+public static class Chain
+{
+ ///
+ /// Replaces a structure in the chain (if present, and is false), or adds it to the end.
+ ///
+ /// The current chain
+ /// A reference to the structure to update
+ /// Always adds to the end of the chain, even if an equivalent structure is present.
+ /// The type of the current chain
+ /// The type of the value
+ /// A reference to the value value in the chain
+ ///
+ /// Note that both the supplied chain, and the supplied value will have their `SType` correctly set. Further,
+ /// the supplied structure's will be overwritten.
+ /// To use
+ ///
+ /// var indexingFeatures = new PhysicalDeviceDescriptorIndexingFeatures
+ /// {
+ /// ShaderInputAttachmentArrayDynamicIndexing = true
+ /// };
+ /// var accelerationStructureFeaturesKhr = new PhysicalDeviceAccelerationStructureFeaturesKhr
+ /// {
+ /// AccelerationStructure = true
+ /// };
+ ///
+ /// PhysicalDeviceFeatures2
+ /// .BaseInStructure(out var features2)
+ /// .SetNext(ref indexingFeatures)
+ /// .SetNext(ref accelerationStructureFeaturesKhr);
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static ref TChain SetNext
+ (
+ this ref TChain chain,
+ ref TNext value,
+ bool alwaysAdd = false
+ )
+ where TChain : struct, IChainStart
+ where TNext : struct, IExtendsChain
+ => ref SetNextAny(ref chain, ref value, alwaysAdd);
+
+ ///
+ /// Replaces a structure in the chain (if present, and is false), or adds it to the end.
+ ///
+ /// The current chain
+ /// A reference to the structure to update
+ /// Always adds to the end of the chain, even if an equivalent structure is present.
+ /// The type of the current chain
+ /// The type of the value
+ /// A reference to the value value in the chain
+ ///
+ /// Note that both the supplied chain, and the supplied value will have their `SType` correctly set. Further,
+ /// the supplied structure's will be overwritten.
+ /// To use
+ ///
+ /// var indexingFeatures = new PhysicalDeviceDescriptorIndexingFeatures
+ /// {
+ /// ShaderInputAttachmentArrayDynamicIndexing = true
+ /// };
+ /// var accelerationStructureFeaturesKhr = new PhysicalDeviceAccelerationStructureFeaturesKhr
+ /// {
+ /// AccelerationStructure = true
+ /// };
+ ///
+ /// PhysicalDeviceFeatures2
+ /// .BaseInStructure(out var features2)
+ /// .SetNext(ref indexingFeatures)
+ /// .SetNext(ref accelerationStructureFeaturesKhr);
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public static unsafe ref TChain SetNextAny
+ (
+ this ref TChain chain,
+ ref TNext value,
+ bool alwaysAdd = false
+ )
+ where TChain : struct, IChainable
+ where TNext : struct, IChainable
+ {...}
+
+ ///
+ /// Adds a structure to the end of the chain.
+ ///
+ /// The current chain
+ /// The structure added to the end of the chain
+ /// The type of the current chain
+ /// The type of the structure to add
+ /// The reference to the chain.
+ ///
+ /// Note that both the supplied chain, and the added structure will have their `SType` correctly set
+ /// To use specify the output type required, e.g.:
+ ///
+ /// PhysicalDeviceFeatures2
+ /// .BaseInStructure(out var features2)
+ /// .AddNext(out PhysicalDeviceDescriptorIndexingFeatures indexingFeatures)
+ /// .AddNext(out PhysicalDeviceAccelerationStructureFeaturesKhr accelerationStructureFeaturesKhr);
+ ///
+ /// Note, the value is always added, even if an equivalent value is added in the chain already. Use
+ /// to only add if not already present.
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static ref TChain AddNext(this ref TChain chain, out TNext next)
+ where TChain : struct, IChainStart
+ where TNext : struct, IExtendsChain
+ => ref AddNextAny(ref chain, out next);
+
+ ///
+ /// Adds a structure to the end of the chain.
+ ///
+ /// The current chain
+ /// The structure added to the end of the chain
+ /// The type of the current chain
+ /// The type of the structure to add
+ /// The reference to the chain.
+ ///
+ /// Note that both the supplied chain, and the added structure will have their `SType` correctly set
+ /// To use specify the output type required, e.g.:
+ ///
+ /// PhysicalDeviceFeatures2
+ /// .BaseInStructure(out var features2)
+ /// .AddNext(out PhysicalDeviceDescriptorIndexingFeatures indexingFeatures)
+ /// .AddNext(out PhysicalDeviceAccelerationStructureFeaturesKhr accelerationStructureFeaturesKhr);
+ ///
+ /// Note, the value is always added, even if an equivalent value is added in the chain already. Use
+ /// to only add if not already present.
+ ///
+ /// The `Any` versions of chain methods do not validate that items belong in the chain, this is
+ /// useful for situations where the specification does not indicate required chain constraints. You should generally
+ /// try to use the none `Any` version in preference.
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public static unsafe ref TChain AddNextAny(this ref TChain chain, out TNext next)
+ where TChain : struct, IChainable
+ where TNext : struct, IChainable
+ {...}
+
+ ///
+ /// Tries to add a structure to the end of the chain.
+ ///
+ /// The current chain
+ /// The structure added to the end of the chain
+ /// Whether the structure was actually added
+ /// The type of the current chain
+ /// The type of the structure to add
+ /// The reference to the chain.
+ ///
+ /// Note that both the supplied chain, and the added structure will have their `SType` correctly set
+ /// To use specify the output type required, e.g.:
+ ///
+ /// PhysicalDeviceFeatures2
+ /// .BaseInStructure(out var features2)
+ /// .TryAddNext(out PhysicalDeviceDescriptorIndexingFeatures indexingFeatures, out var added);
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static ref TChain TryAddNext(this ref TChain chain, out TNext next, out bool added)
+ where TChain : struct, IChainStart
+ where TNext : struct, IExtendsChain
+ => ref TryAddNextAny(ref chain, out next, out added);
+
+ ///
+ /// Tries to add a structure to the end of the chain.
+ ///
+ /// The current chain
+ /// The structure added to the end of the chain
+ /// Whether the structure was actually added
+ /// The type of the current chain
+ /// The type of the structure to add
+ /// The reference to the chain.
+ ///
+ /// Note that both the supplied chain, and the added structure will have their `SType` correctly set
+ /// To use specify the output type required, e.g.:
+ ///
+ /// PhysicalDeviceFeatures2
+ /// .BaseInStructure(out var features2)
+ /// .TryAddNext(out PhysicalDeviceDescriptorIndexingFeatures indexingFeatures, out var added);
+ ///
+ ///
+ /// The `Any` versions of chain methods do not validate that items belong in the chain, this is
+ /// useful for situations where the specification does not indicate required chain constraints. You should generally
+ /// try to use the none `Any` version in preference.
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public static unsafe ref TChain TryAddNextAny(this ref TChain chain, out TNext next, out bool added)
+ where TChain : struct, IChainable
+ where TNext : struct, IChainable
+ {...}
+
+ ///
+ /// Returns the index of the in the , if present.
+ ///
+ /// The chain
+ /// The structure value
+ /// The type of the current chain
+ /// The type of the value
+ /// The zero-indexed index if found; otherwise -1.
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static int IndexOf(this ref TChain chain, ref TNext value)
+ where TChain : struct, IChainStart
+ where TNext : struct, IExtendsChain
+ => IndexOfAny(ref chain, ref value);
+
+ ///
+ /// Returns the index of the in the , if present.
+ ///
+ /// The chain
+ /// The structure value
+ /// The type of the current chain
+ /// The type of the value
+ /// The zero-indexed index if found; otherwise -1.
+ ///
+ /// The `Any` versions of chain methods do not validate that items belong in the chain, this is
+ /// useful for situations where the specification does not indicate required chain constraints. You should generally
+ /// try to use the none `Any` version in preference.
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public static unsafe int IndexOfAny(this ref TChain chain, ref TNext value)
+ where TChain : struct, IChainable
+ where TNext : struct, IChainable
+ {...}
+}
+```
+
+### Chain Structure
+
+The `BaseInStructure` struct makes it easy to access the `SType` and `PNext` of a structure pointed to by `void* PNext`,
+although it is used internally, it is useful for consumers of Silk.Net to have access to use in their own scenarios,
+that is because the `IChainable` interface does not directly expose the underlying `SType` and `PNext` fields; as they
+are fields (not properties), and this proposal aims to avoid boxing (so we try not to use the interface directly
+unnecessarily).
+
+```csharp
+namespace Silk.Net.Vulkan;
+
+///
+/// Header struct of all structs.
+///
+///
+/// Any pointer to a structure marked as can safely be cast to a pointer to this type.
+/// In particular, this means that the void* PNext field can always be safely cast to BaseInStructure*, providing
+/// access to the `SType` and `PNext` fields.
+///
+///
+public struct Chain : IChainable
+{
+ ///
+ /// The structure type.
+ ///
+ public StructureType SType;
+ ///
+ /// The next struct in the chain, if any; otherwise .
+ ///
+ public unsafe BaseInStructure* PNext;
+
+ ///
+ /// Note, this cannot coerce the type as 'guaranteed by the `IStructuredType` interface.
+ StructureType IStructuredType.StructureType()
+ {
+ return SType;
+ }
+}
+```
+
+### Static Chain implementation
+
+Whenever the `IChainStart` interface is added to an `IChainable` struct, the following static convenience methods is
+also added:
+
+```csharp
+///
+/// Convenience method to start a chain.
+///
+/// The newly created chain root
+/// A reference to the newly created chain.
+public static unsafe ref [StrucType] Chain(
+ out [StructType] capture)
+{
+ capture = new [StructType](StructureType.[StructureType]);
+ return ref capture;
+}
+```
+
+### Chain interfaces (Optional)
+
+As a useful optional extra, whenever an `IChainStart` struct is found, a corresponding `I[StructName]Chain` is created,
+that extends from `IChainable`, e.g.:
+
+```csharp
+namespace Silk.Net.Vulkan;
+
+///
+/// Marks a chainable struct as being part of the `DeviceCreateInfo` chain.
+///
+public interface IDeviceCreateInfoChain : IChainable
+{
+}
+```
+
+This interface is then added to the corresponding `IChainStart` _and_ any struct that implements the
+corresponding `IExtendsChain`. The primary benefit of this approach is to make it significantly easier to write
+code that accepts any part of a specific chain (including the head).
\ No newline at end of file
diff --git a/documentation/proposals/Proposal - Vulkan Struct Chaining - #3 Managed Chaining.md b/documentation/proposals/Proposal - Vulkan Struct Chaining - #3 Managed Chaining.md
new file mode 100644
index 0000000000..d055b133ee
--- /dev/null
+++ b/documentation/proposals/Proposal - Vulkan Struct Chaining - #3 Managed Chaining.md
@@ -0,0 +1,1027 @@
+# Summary
+
+**_This proposal is dependent
+on [Proposal - Vulkan Struct Chaining - #2 Unmanaged Chaining](Proposal%20-%20Vulkan%20Struct%20Chaining%20-%20%232%20Unmanaged%20Chaining.md)
+._**
+
+This proposal presents a _managed_ mechanism for safely building, and storing, Vulkan Structure Chains. You may wish to
+start with the [previous proposal](Proposal%20-%20Vulkan%20Struct%20Chaining%20-%20%232%20Unmanaged%20Chaining.md),
+followed by the [Usage section below](#Usage) to aid understanding. There is also a fully working prototype
+[in the labs](../../src/Lab/Experiments/PrototypeStructChaining/PrototypeStructChaining).
+
+The [previous proposal](Proposal%20-%20Vulkan%20Struct%20Chaining%20-%20%232%20Unmanaged%20Chaining.md) provided a
+lightweight mechanism for building structure chains, but the responsibility for ensuring that the provided structures
+did not move remained with the consumer. This provided a useful mechanism for reducing bugs (through compile time and
+run time validation of `SType` and `PNext`) and making more readable and compact code, whilst not sacrificing
+performance.
+
+However, many consumers are uncomfortable with pointers, and are especially prone to introducing bugs when placing
+structs onto the heap. This proposal provides a convenient `ManagedChain` class, and multiple
+descendent `ManagedChain` (similar to the `System.Tuple` classes) classes to safely fix the structures
+in memory and prevent pointer bugs.
+
+Whenever a structure is loaded into the `ManagedChain` its `SType` and `PNext` are forced to be correct, preventing
+errors. Structures can be replaced at any time, and will be inserted efficiently into the chain as an O(1) operation.
+
+# Contributors
+
+- [Craig Dean, DevDecoder](https://github.com/thargy)
+
+# Current Status
+
+- [x] Proposed
+- [ ] Discussed with API Review Board (ARB)
+- [ ] Approved
+- [ ] Implemented
+
+# Design Decisions
+
+- There are no requirements to extend `BuildTools`, or add any additional information to the `IChainable` structures.
+- Although the `ManagedChain` generic classes are auto-generated (for convenience) this is done using T4
+ templating, an implementation of which is
+ provided [in the labs](../../src/Lab/Experiments/PrototypeStructChaining/PrototypeStructChaining/ManagedChain.gen.tt).
+- For improved performance, the chain's structures are held in a single block of contiguous unmanaged memory, as the
+ memory is unmanaged, the position of the structures remains fixed, even though the containing object can be safely
+ moved around by the GC in the heap. Indeed a pointer to this block is the only data stored by an instance of any
+ `ManageChain` object, despite all the functionality provided!
+- The structure accessors return a copy of the structures, and always correct the `SType` and `PNext` on input. Even
+ though the `PNext` values are exposed there is no way to modify them from outside the class, guaranteeing their
+ safety.
+- The managed chains implement `IEquatable`, allowing two chains with identical content to be efficiently compared (
+ ignoring the `PNext` pointers, but already being confident the `SType` and ordering is correct). They also implement
+ the equality operator overloads, and `GetHashCode`.
+
+Open questions:
+
+- Should we expose 8, 16 or 32 `ManagedChain` classes?
+- Do we want to stick with `TChain chain, T1 item1`, or use `T1 item1, T2 item2` ala `Tuple`?
+- Although the constructors used by `ManagedChain.Create` and `ManagedChain.Load` could be made `internal`, I don't
+ propose we do so. Primarily, the main benefit of the static methods is type inference, but, as chain building is
+ frequently done with defaults then direct constructor access does not have a disadvantage, and can take advantage of
+ implicit typing when assigning to an already typed field/property. Having both forms available is therefore
+ convenient.
+- The current `Load` methods expect an unmanaged chain that matches the supplied type constraints, and is of the same
+ length. This is useful, as coders will normally expect a particular chain. We could additionally add more lax `Import`
+ methods that will import an unmanaged chain into a managed chain by populating any positions with structure types
+ found in the unmanaged chain, no matter at what position they are found. This is not entirely unreasonable as the
+ order of chains (after the start) is not fixed in Vulkan, and it will allow importing existing chains where the order
+ doesn't matter.
+- `GetHasCode` current hashes the entire struct's data, except the `PNext` fields. However, a hashcode only needs to
+ create reasonable separation so a 'sampling' method could be used for increased performance.
+- Similar to `Append` and `Truncate` we could also add `Insert` and `Remove` methods, though slightly more complex, as
+ we'd have to generate multiples of each, it is not difficult to do, for example:
+
+```csharp
+pubilc class ManagedCache ... {
+ ...
+
+ // There would be of these methods (not too bad to be fair, the worst case would be maxsize -1 as we
+ // wouldn't add these methods to the largest possible ManagedCache)
+ public ManagedCache InsertBeforeHead(TChain newHead) {...}
+ public ManagedCache InsertBeforeItem1(TNew newValue) {...}
+ public ManagedCache InsertBeforeItem2(TNew newValue) {...}
+
+ // There would be size-2 of these methods (so wouldn't exist on `ManagedCache`)
+ public ManagedCache RemoveItem1(out T1 removedValue) {...}
+
+ // This is pretty trivial to add (also wouldn't exist on `ManagedCache`)
+ public ManagedCache Truncate(out T2 removedValue) {...}
+}
+```
+
+# Usage
+
+## The Any versions
+
+As with unmanaged chains, the managed chains system includes `Any` versions of all methods. In fact,
+the `ManagedChain` constraints are 'loose', that is they only require types to be `IChainable` rather than
+requiring the stricter constraints that prevent unrelated chain elements being added, or used as the start of a chain.
+
+With the exception of the setters on chain items, you cannot manipulate a chain save through the static methods, and the
+preferred versions do include the tighter constraints.
+
+## Instance-Based Methods
+
+### Creation (Create/CreateAny)
+
+The following will create a chain starting with `PhysicalDeviceFeatures2`, pointing
+to `PhysicalDeviceDescriptorIndexingFeatures` and finishing with a `PhysicalDeviceAccelerationStructureFeaturesKHR`
+structure:
+
+```csharp
+using var chain = ManagedChain.Create
+ (
+ default(PhysicalDeviceFeatures2),
+ default(PhysicalDeviceDescriptorIndexingFeatures),
+ default(PhysicalDeviceAccelerationStructureFeaturesKhr)
+ );
+
+// Ensure all STypes set correctly
+Assert.Equal(StructureType.PhysicalDeviceFeatures2, chain.Head.SType);
+Assert.Equal(StructureType.PhysicalDeviceDescriptorIndexingFeatures, chain.Item1.SType);
+Assert.Equal(StructureType.PhysicalDeviceAccelerationStructureFeaturesKhr, chain.Item2.SType);
+
+// Ensure pointers set correctly
+Assert.Equal((nint) chain.Item1Ptr, (nint) chain.Head.PNext);
+Assert.Equal((nint) chain.Item2Ptr, (nint) chain.Item1.PNext);
+Assert.Equal((nint) 0, (nint) chain.Item2.PNext);
+```
+
+The structures are held in unmanaged memory, preventing movement by the GC, and ensuring that the pointers remain fixed.
+
+You can also use specify the generic types directly, e.g.:
+
+```csharp
+using var chain = ManagedChain.Create<
+ DeviceCreateInfo,
+ PhysicalDeviceFeatures2,
+ PhysicalDeviceDescriptorIndexingFeatures>();
+```
+
+### Modifying values (Head/Item# properties)
+
+We can easily modify any value in the `ManagedChain`, and it will maintain the pointers automatically. You do this using
+the `Head` property, or one of the `Item#` properties (e.g. `Item1`), for example:
+
+```csharp
+using var chain = ManagedChain.Create<
+ DeviceCreateInfo,
+ PhysicalDeviceFeatures2,
+ PhysicalDeviceDescriptorIndexingFeatures>();
+
+// Ensure all STypes set correctly
+Assert.Equal(StructureType.DeviceCreateInfo, chain.Head.SType);
+Assert.Equal(StructureType.PhysicalDeviceFeatures2, chain.Item1.SType);
+Assert.Equal(StructureType.PhysicalDeviceDescriptorIndexingFeatures, chain.Item2.SType);
+
+// Ensure pointers set correctly
+Assert.Equal((nint) chain.Item1Ptr, (nint) chain.Head.PNext);
+Assert.Equal((nint) chain.Item2Ptr, (nint) chain.Item1.PNext);
+Assert.Equal((nint) 0, (nint) chain.Item2.PNext);
+
+Assert.Equal(0U, chain.Head.Flags);
+
+var headPtr = chain.HeadPtr;
+
+// Get the current head (this is a copy)
+var head = chain.Head;
+// Update the flags
+head.Flags = 1U;
+// Update the chain
+chain.Head = head;
+
+Assert.Equal(1U, chain.Head.Flags);
+
+// The head ptr should not change, as we overwrite the same memory location with the new value
+Assert.Equal((nint) headPtr, (nint) chain.HeadPtr);
+// But the next pointer should not change
+Assert.Equal((nint) chain.Item1Ptr, (nint) chain.Head.PNext);
+```
+
+**Note** When we update any item in the chain it overwrites the existing memory, so the pointers remain fixed. It also
+ensures the `PNext` value pointing to it is maintained.
+
+## Extension Methods
+
+### Appending to a chain (Append/AppendAny)
+
+You can call `Append` on a `ManagedChain` (of length < 16) to efficiently create a new, larger, `ManagedChain` with a
+new item appended to the end, e.g:
+
+```csharp
+using var chain = ManagedChain.Create(
+ item1: new PhysicalDeviceDescriptorIndexingFeatures {ShaderInputAttachmentArrayDynamicIndexing = true});
+
+// The new chain, will efficiently copy the old chain and append a new structure to the end
+using var newChain = chain.Append();
+// You will usualy wish to dispose the old chain here, the two chains are now independent of each other.
+
+// Check the flag from the first chain is still set in the new chain.
+Assert.True(newChain.Item1.ShaderInputAttachmentArrayDynamicIndexing);
+```
+
+**Note** As a `ManagedChain` holds a block of unmanaged memory, it must be [disposed manually](#disposal) when it is
+finished with, when using the `Append` method you will produce a new `ManagedChain` and should not forget to dispose the
+original if it is no longer needed.
+
+### Truncating (Truncate/TruncateAny)
+
+Similarly, you can `Truncate` a chain (of length > 1) to get an instance of a smaller chain:
+
+```csharp
+using var chain = ManagedChain.Create();
+using var chain2 = chain.Append();
+// Remove the indexing features we just added (note the out parameter is optional)
+using var chain3 = chain2.Truncate(out var indexingFeatures);
+
+Assert.Equal(1, chain.Count);
+Assert.Equal(2, chain2.Count);
+Assert.Equal(1, chain3.Count);
+```
+
+### Duplication (Duplicate/DuplicateAny)
+
+You can efficiently duplicate a managed chain by calling Duplicate on it:
+
+```csharp
+using var chain = new ManagedChain.Create();
+using var copy = chain.Duplicate();
+
+// Test equality
+Assert.Equal(chain, copy);
+Assert.True(chain == copy);
+```
+
+**Note** The `copy` is 'equal' to the `chain` until you modify it's contents.
+
+### Loading from an unmanaged chain (Load/LoadAny)
+
+If you have created an unmanaged chain and would like to load that into a `ManagedChain` you can use one of the
+`ManagedChain.Load` methods:
+
+```csharp
+// Load an unmanaged chain
+var indexingFeatures = new PhysicalDeviceDescriptorIndexingFeatures
+{
+ ShaderInputAttachmentArrayDynamicIndexing = true
+};
+PhysicalDeviceFeatures2
+.Chain(out var unmanagedChain)
+.SetNext(ref indexingFeatures)
+.AddNext(out PhysicalDeviceAccelerationStructureFeaturesKHR accelerationStructureFeaturesKhr);
+
+// Loads a new managed chain from an unmanaged chain
+using var managedChain =
+ ManagedChain.Load(out var errors, unmanagedChain);
+
+// Check we had no loading errors
+Assert.Equal(string.Empty, errors);
+
+// Check the flag still set
+Assert.True(managedChain.Item1.ShaderInputAttachmentArrayDynamicIndexing);
+```
+
+The full version of the `Load` method returns an output parameter `errors` as it's first parameter. The `errors`
+parameter will be `string.Empty` if there are no errors, otherwise each line will contain a separate error for each
+issue found during loading. There is also an overload that accepts a single argument `chain` for when you don't care if
+there are any errors. Either method always succeeds, even if the unmanaged chain doesn't match exactly - for example it
+is shorter or longer than the chain being loaded, or if the managed chain has different structure types in any of the
+positions. Any structure type in the expected position will always be loaded into the new `ManagedChain`.
+
+```csharp
+var indexingFeatures = new PhysicalDeviceDescriptorIndexingFeatures
+{
+ ShaderInputAttachmentArrayDynamicIndexing = true
+};
+// Load an unmanaged chain
+DeviceCreateInfo
+ .Chain(out var unmanagedChain)
+ .AddNext(out PhysicalDeviceFeatures2 features2)
+ .SetNext(ref indexingFeatures)
+ .AddNext(out PhysicalDeviceAccelerationStructureFeaturesKHR accelerationStructureFeaturesKhr);
+
+// Loads a new managed chain from an unmanaged chain
+using var managedChain =
+ new ManagedChain<
+ DeviceCreateInfo,
+ // Note we are supplied a PhysicalDeviceFeatures2 here from the unmanaged chain
+ PhysicalDeviceAccelerationStructureFeaturesKHR,
+ PhysicalDeviceDescriptorIndexingFeatures,
+ PhysicalDeviceAccelerationStructureFeaturesKHR,
+ // Note that the unmanaged chain did not supply a 5th entry
+ PhysicalDeviceFeatures2>(out var errors, unmanagedChain );
+
+// Check for errors
+Assert.Equal(
+@"The unmanaged chain has a structure type PhysicalDeviceFeatures2Khr at position 2; expected PhysicalDeviceAccelerationStructureFeaturesKhr
+The unmanaged chain was length 4, expected length 5",
+ errors);
+
+// Despite the errors indexing features was at the right location so was loaded
+Assert.True(managedChain.Item2.ShaderInputAttachmentArrayDynamicIndexing);
+```
+
+Notice that the above form uses the equivalent constructor as an alternative to the `Load` method. There is no
+equivalent constructor to `Load(TChain)` as that would be ambiguous.
+
+## Additional interfaces
+
+### IReadOnlyList
+
+All the fully generic `ManageChain` types extend `ManagedChain` which implements `IDisposable`
+and `IReadOnlyList`. The latter allowing for easy consumption of any `ManagedChain`, e.g.:
+
+```csharp
+using var chain = new ManagedChain();
+
+Assert.Equal(3, chain.Count);
+
+// Ensure all STypes set correctly using indexer
+Assert.Equal(StructureType.PhysicalDeviceFeatures2, chain[0].StructureType());
+Assert.Equal(StructureType.PhysicalDeviceDescriptorIndexingFeatures, chain[1].StructureType());
+Assert.Equal(StructureType.PhysicalDeviceAccelerationStructureFeaturesKhr, chain[2].StructureType());
+
+Assert.Throws(() => chain[3]);
+
+// Get array using IEnumerable implementation
+IChainable[] structures = chain.ToArray();
+
+// Check concrete types
+Assert.IsType(structures[0]);
+Assert.IsType(structures[1]);
+Assert.IsType(structures[2]);
+```
+
+### Equality (IEquatable<T>)
+
+ll the fully generic `ManageChain` types implement the corresponding `IEquatable>`
+interface, and equality operators. As well as `GetHashCode`.
+
+### Deconstruction
+
+Each `ManageChain` has a corresponding deconstructor for convenience, e.g.:
+
+```csharp
+using var chain = new ManagedChain();
+
+var (physicalDeviceFeatures2, indexingFeatures, accelerationStructureFeaturesKhr) = chain;
+
+// Ensure all STypes set correctly
+Assert.Equal(StructureType.PhysicalDeviceFeatures2, physicalDeviceFeatures2.SType);
+Assert.Equal(StructureType.PhysicalDeviceDescriptorIndexingFeatures, indexingFeatures.SType);
+Assert.Equal(StructureType.PhysicalDeviceAccelerationStructureFeaturesKhr, accelerationStructureFeaturesKhr.SType);
+```
+
+### Disposal (IDisposable)
+
+As each `ManagedChain` holds the underlying structures in unmanaged memory (to prevent them being moved and their
+pointers being invalidated), then it is critical you dispose them; either by calling `Dispose()` or by using a `using`
+statement.
+
+# Proposed API
+
+## Abstract base class
+
+The `ManagedChain`, non-generic abstract base class provides an abstract implementation of `IReadOnlyList`,
+and defines static `Create` and `Load` methods for each size of chain.
+
+```csharp
+
+///
+/// Base class for all Managed Chains.
+///
+public abstract unsafe class ManagedChain : IReadOnlyList, IDisposable
+{
+ ///
+ /// Gets a pointer to the current head.
+ ///
+ public abstract BaseInStructure* HeadPtr { get; }
+
+ ///
+ /// Gets the total size (in bytes) of the unmanaged memory, managed by this chain.
+ ///
+ public abstract int Size { get; }
+
+ ///
+ public abstract IEnumerator GetEnumerator();
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return GetEnumerator();
+ }
+
+ ///
+ public abstract int Count { get; }
+
+ ///
+ public abstract IChainable this[int index] { get; }
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public override bool Equals(object obj)
+ {
+ return !ReferenceEquals(null, obj) &&
+ (ReferenceEquals(this, obj) || obj.GetType() == this.GetType() && MemoryEquals((ManagedChain) obj));
+ }
+
+ ///
+ /// Compares the supplied memory block with this one.
+ ///
+ protected abstract bool MemoryEquals(ManagedChain other);
+
+ ///
+ public abstract void Dispose();
+
+ ///
+ /// Combines a hashcode with the first part of a slice.
+ ///
+ ///
+ ///
+ ///
+ protected static void CombineHash(ref int hashCode, ReadOnlySpan slice) =>
+ hashCode = slice.Length switch
+ {
+ < 2 => HashCode.Combine(hashCode, slice[0]),
+ < 4 => HashCode.Combine(hashCode, MemoryMarshal.Cast(slice)[0]),
+ < 8 => HashCode.Combine(hashCode, MemoryMarshal.Cast(slice)[0]),
+ _ => HashCode.Combine(hashCode, MemoryMarshal.Cast(slice)[0])
+ };
+
+ // Skipping methods for `ManagedChain{TChain}` to show more completed example
+ ...
+
+ ///
+ /// Creates a new with 2 items.
+ ///
+ /// The head of the chain.
+ /// Item 1.
+ /// The chain type
+ /// Type of Item 1.
+ /// A new with 2 items.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static ManagedChain Create(TChain head = default, T1 item1 = default)
+ where TChain : struct, IChainStart
+ where T1 : struct, IExtendsChain
+ => new(head, item1);
+
+ ///
+ /// Creates a new with 2 items.
+ ///
+ /// The head of the chain.
+ /// Item 1.
+ /// The chain type
+ /// Type of Item 1.
+ /// A new with 2 items.
+ /// The `Any` versions of chain methods do not validate that items belong in the chain, this is
+ /// useful for situations where the specification does not indicate required chain constraints. You should generally
+ /// try to use the none `Any` version in preference.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static ManagedChain CreateAny(TChain head = default, T1 item1 = default)
+ where TChain : struct, IChainable
+ where T1 : struct, IChainable
+ => new(head, item1);
+
+ ///
+ /// Loads a new with 2 items from an existing unmanaged chain,
+ /// ignoring any errors.
+ ///
+ /// The unmanaged chain to use as the basis of this chain.
+ /// A new with 2 items.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static ManagedChain Load(TChain chain)
+ where TChain : struct, IChainStart
+ where T1 : struct, IExtendsChain
+ => LoadAny(out var _, chain);
+
+ ///
+ /// Loads a new with 2 items from an existing unmanaged chain,
+ /// ignoring any errors.
+ ///
+ /// The unmanaged chain to use as the basis of this chain.
+ /// A new with 2 items.
+ /// The `Any` versions of chain methods do not validate that items belong in the chain, this is
+ /// useful for situations where the specification does not indicate required chain constraints. You should generally
+ /// try to use the none `Any` version in preference.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static ManagedChain LoadAny(TChain chain)
+ where TChain : struct, IChainable
+ where T1 : struct, IChainable
+ => LoadAny(out var _, chain);
+
+ ///
+ /// Loads a new with 2 items from an existing unmanaged chain.
+ ///
+ /// Any errors loading the chain.
+ /// The unmanaged chain to use as the basis of this chain.
+ /// A new with 2 items.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static ManagedChain Load(out string errors, TChain chain)
+ where TChain : struct, IChainStart
+ where T1 : struct, IExtendsChain
+ => LoadAny(out errors, chain);
+
+ ///
+ /// Loads a new with 2 items from an existing unmanaged chain.
+ ///
+ /// Any errors loading the chain.
+ /// The unmanaged chain to use as the basis of this chain.
+ /// A new with 2 items.
+ /// The `Any` versions of chain methods do not validate that items belong in the chain, this is
+ /// useful for situations where the specification does not indicate required chain constraints. You should generally
+ /// try to use the none `Any` version in preference.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static ManagedChain LoadAny(out string errors, TChain chain)
+ where TChain : struct, IChainable
+ where T1 : struct, IChainable
+ {
+ var size = ManagedChain.MemorySize;
+ var newHeadPtr = Marshal.AllocHGlobal(size);
+ chain.StructureType();
+ Marshal.StructureToPtr(chain, newHeadPtr, false);
+ var errorBuilder = new StringBuilder();
+ var existingPtr = (BaseInStructure*) Unsafe.AsPointer(ref chain);
+ var newPtr = (BaseInStructure*) newHeadPtr;
+
+ existingPtr = existingPtr->PNext;
+ newPtr->PNext = (BaseInStructure*) (newHeadPtr + ManagedChain.Item1Offset);
+ newPtr = newPtr->PNext;
+
+ T1 item1 = default;
+ var expectedStructureType = item1.StructureType();
+ if (existingPtr is null) {
+ errorBuilder.AppendLine("The unmanaged chain was length 1, expected length 2");
+ } else {
+ if (existingPtr->SType != expectedStructureType) {
+ errorBuilder.Append("The unmanaged chain has a structure type ")
+ .Append(existingPtr->SType)
+ .Append(" at position 2; expected ")
+ .Append(expectedStructureType)
+ .AppendLine();
+ } else {
+ if (existingPtr->PNext is not null) {
+ errorBuilder.AppendLine("The unmanaged chain was longer than the expected length 2");
+ existingPtr->PNext = null;
+ }
+ item1 = Unsafe.AsRef(existingPtr);
+ }
+ }
+ Marshal.StructureToPtr(item1, (nint) newPtr, false);
+
+ // Create string of errors
+ errors = errorBuilder.ToString().Trim();
+ return new ManagedChain(newHeadPtr);
+ }
+
+ // Only showing one example of Create/Load methods
+ ...
+}
+```
+
+## Concrete generic classes
+
+A class is generated for each valid size of a chain, here is one example:
+
+```csharp
+///
+/// A safely manages the pointers of a managed structure chain.
+///
+/// The chain type
+/// Type of Item 1.
+public unsafe sealed class ManagedChain : ManagedChain, IEquatable>
+ where TChain : struct, IChainable
+ where T1 : struct, IChainable
+{
+ ///
+ /// Gets the size (in bytes) of the default structure header.
+ ///
+ public static readonly int HeaderSize = Marshal.SizeOf();
+
+ ///
+ /// Gets the size (in bytes) of the head structure.
+ ///
+ public static readonly int HeadSize = Marshal.SizeOf();
+
+ ///
+ /// Gets the offset to the start of .
+ ///
+ public static readonly int Item1Offset = HeadSize;
+
+ ///
+ /// Gets the size (in bytes) of the Item 1.
+ ///
+ public static readonly int Item1Size = Marshal.SizeOf();
+
+ ///
+ /// Gets the total size (in bytes) of the unmanaged memory, managed by this chain.
+ ///
+ public static readonly int MemorySize = Item1Offset + Item1Size;
+
+ ///
+ public override int Size => MemorySize;
+
+ private nint _headPtr;
+
+ ///
+ public override BaseInStructure* HeadPtr => (BaseInStructure*) _headPtr;
+
+ ///
+ /// Gets or sets the head of the chain.
+ ///
+ public TChain Head
+ {
+ get => Unsafe.AsRef((BaseInStructure*) _headPtr);
+ set
+ {
+ value.StructureType();
+ var ptr = (BaseInStructure*) _headPtr;
+ var nextPtr = ptr->PNext;
+ Marshal.StructureToPtr(value, _headPtr, true);
+ ptr->PNext = nextPtr;
+ }
+ }
+
+ ///
+ /// Gets a pointer to the second item in the chain.
+ ///
+ public BaseInStructure* Item1Ptr => (BaseInStructure*) (_headPtr + Item1Offset);
+
+ ///
+ /// Gets or sets item #1 in the chain.
+ ///
+ public T1 Item1
+ {
+ get => Unsafe.AsRef(Item1Ptr);
+ set
+ {
+ value.StructureType();
+ var ptr = Item1Ptr;
+ var nextPtr = ptr->PNext;
+ Marshal.StructureToPtr(value, (nint)ptr, true);
+ ptr->PNext = nextPtr;
+ }
+ }
+
+ ///
+ /// Creates a new with 2 items from an existing memory block.
+ ///
+ /// The pointer to the head of the chain.
+ ///
+ /// Callers are responsible for ensuring the size of the memory is correct.
+ ///
+ internal ManagedChain(nint headPtr)
+ {
+ _headPtr = headPtr;
+ }
+
+ ///
+ /// Creates a new with 2 items.
+ ///
+ /// The head of the chain.
+ /// Item 1.
+ internal ManagedChain(TChain head = default, T1 item1 = default)
+ : this(Marshal.AllocHGlobal(MemorySize))
+ {
+ head.StructureType();
+ Marshal.StructureToPtr(head, _headPtr, false);
+ var itemPtr = Item1Ptr;
+ item1.StructureType();
+ Marshal.StructureToPtr(item1, (nint)itemPtr, false);
+ HeadPtr->PNext = itemPtr;
+ Item1Ptr->PNext = null;
+ }
+
+ ///
+ public override IEnumerator GetEnumerator()
+ {
+ yield return Head;
+ yield return Item1;
+ }
+
+ ///
+ public override int Count => 2;
+
+ ///
+ public override IChainable this[int index]
+ => index switch
+ {
+ 0 => Head,
+ 1 => Item1,
+ _ => throw new IndexOutOfRangeException()
+ };
+
+ ///
+ /// Compares the supplied memory block with this one, ignoring the structure headers.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ protected override bool MemoryEquals(ManagedChain other)
+ {
+ var ptr = HeadPtr;
+ var otherPtr = other.HeadPtr;
+ if (ptr == otherPtr) {
+ return true;
+ }
+ var span = new ReadOnlySpan((void*) ptr, MemorySize);
+ var otherSpan = new ReadOnlySpan((void*) otherPtr, MemorySize);
+ var start = 0;
+ var length = HeadSize;
+ var sliceLength = length - HeaderSize;
+ if (sliceLength > 0 &&
+ !span.Slice(start + HeaderSize, sliceLength)
+ .SequenceEqual(otherSpan.Slice(start + HeaderSize, sliceLength)))
+ return false;
+
+ start += length;
+ length = Item1Size;
+ sliceLength = length - HeaderSize;
+ if (sliceLength > 0 &&
+ !span.Slice(start + HeaderSize, sliceLength)
+ .SequenceEqual(otherSpan.Slice(start + HeaderSize, sliceLength)))
+ return false;
+ return true;
+ }
+
+ ///
+ public override int GetHashCode()
+ {
+ var span = new ReadOnlySpan((void*)_headPtr, MemorySize);
+ var start = 0;
+ var length = HeadSize;
+ var sliceLength = length - HeaderSize;
+ var hashCode = 0;
+ // Hash the structure type
+ var sTYpe = ((BaseInStructure*) (_headPtr + start))->SType;
+ hashCode = HashCode.Combine(hashCode, sTYpe);
+
+ // Hash any payload
+ if (sliceLength >= 0)
+ CombineHash(ref hashCode, span.Slice(start + HeaderSize, sliceLength));
+
+ start += length;
+ length = Item1Size;
+ sliceLength = length - HeaderSize;
+ sTYpe = ((BaseInStructure*) (_headPtr + start))->SType;
+ hashCode = HashCode.Combine(hashCode, sTYpe);
+ if (sliceLength >= 0)
+ CombineHash(ref hashCode, span.Slice(start + HeaderSize, sliceLength));
+ return hashCode;
+ }
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public bool Equals(ManagedChain other)
+ => !ReferenceEquals(null, other) && (ReferenceEquals(this, other) || MemoryEquals(other));
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool operator ==(ManagedChain left, ManagedChain right) =>
+ ReferenceEquals(null, left)
+ ? ReferenceEquals(null, right)
+ : !ReferenceEquals(null, right) && (ReferenceEquals(left, right) || left.MemoryEquals(right));
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool operator !=(ManagedChain left, ManagedChain right) =>
+ ReferenceEquals(null, left)
+ ? !ReferenceEquals(null, right)
+ : ReferenceEquals(null, right) || (!ReferenceEquals(left, right) && !left.MemoryEquals(right));
+
+ ///
+ public override string ToString()
+ {
+ StringBuilder sb = new StringBuilder();
+ sb.Append("(");
+ sb.Append((object) Head); sb.Append(", ");
+ sb.Append((object) Item1);
+ sb.Append(")");
+ return sb.ToString();
+ }
+
+ ///
+ /// Deconstructs this chain.
+ ///
+ /// The head of the chain.
+ /// Item 1.
+ public void Deconstruct(out TChain head, out T1 item1)
+ {
+ head = Head;
+ item1 = Item1;
+ }
+
+ ///
+ public override void Dispose()
+ {
+ var headPtr = Interlocked.Exchange(ref _headPtr, (nint)0);
+ if (headPtr == (nint)0) {
+ return;
+ }
+
+ // Destroy all structures
+ Marshal.DestroyStructure(headPtr);
+ Marshal.DestroyStructure(headPtr + Item1Offset);
+
+ // Free memory block
+ Marshal.FreeHGlobal(headPtr);
+ }
+}
+```
+
+## Extension methods
+
+A static class is generated to hold the extension methods (showing one example set):
+
+```csharp
+
+///
+/// Static class providing extension methods for manipulating managed chains.
+///
+/// The `Any` versions of chain methods do not validate that items belong in the chain, this is
+/// useful for situations where the specification does not indicate required chain constraints. You should generally
+/// try to use the none `Any` version in preference.
+public static unsafe class ManagedChainExtensions
+{
+ ...
+
+ ///
+ /// Creates a new with 2 items, by appending to
+ /// the end of the .
+ ///
+ /// The chain.
+ /// The item to append.
+ /// The chain type
+ /// Type of Item 1.
+ ///
+ /// Do not forget to dispose this chain if you are no longer using it.
+ /// The `Any` versions of chain methods do not validate that items belong in the chain, this is
+ /// useful for situations where the specification does not indicate required chain constraints. You should generally
+ /// try to use the none `Any` version in preference.
+ ///
+ ///
+ public static ManagedChain Add(this ManagedChain chain, T1 item1 = default)
+ where TChain : struct, IChainStart
+ where T1 : struct, IExtendsChain
+ => chain.AddAny(item1);
+
+ ///
+ /// Creates a new with 2 items, by appending to
+ /// the end of the .
+ ///
+ /// The chain.
+ /// The item to append.
+ /// The chain type
+ /// Type of Item 1.
+ ///
+ /// Do not forget to dispose this chain if you are no longer using it.
+ /// The `Any` versions of chain methods do not validate that items belong in the chain, this is
+ /// useful for situations where the specification does not indicate required chain constraints. You should generally
+ /// try to use the none `Any` version in preference.
+ ///
+ ///
+ public static ManagedChain AddAny(this ManagedChain chain, T1 item1 = default)
+ where TChain : struct, IChainable
+ where T1 : struct, IChainable
+ {
+ var previousSize = ManagedChain.MemorySize;
+ var newSize = ManagedChain.MemorySize;
+ var newHeadPtr = Marshal.AllocHGlobal(newSize);
+ // Block copy original struct data for speed
+ System.Buffer.MemoryCopy(chain.HeadPtr, (void*)newHeadPtr, previousSize, previousSize);
+
+ // Append item 0
+ item1.StructureType();
+ Marshal.StructureToPtr(item1, newHeadPtr + previousSize, false);
+
+ // Update all pointers
+ ((BaseInStructure*)newHeadPtr)->PNext = (BaseInStructure*) (newHeadPtr + ManagedChain.Item1Offset);
+ ((BaseInStructure*)(newHeadPtr + previousSize))->PNext = null;
+ return new ManagedChain(newHeadPtr);
+ }
+
+ ///
+ /// Creates a new with 2 by copying the .
+ ///
+ /// The chain.
+ /// The chain type
+ /// Type of Item 1.
+ ///
+ /// Do not forget to dispose this chain if you are no longer using it.
+ ///
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static ManagedChain Duplicate(this ManagedChain chain)
+ where TChain : struct, IChainStart
+ where T1 : struct, IExtendsChain
+ => chain.DuplicateAny();
+
+ ///
+ /// Creates a new with 2 by copying the .
+ ///
+ /// The chain.
+ /// The chain type
+ /// Type of Item 1.
+ ///
+ /// Do not forget to dispose this chain if you are no longer using it.
+ /// The `Any` versions of chain methods do not validate that items belong in the chain, this is
+ /// useful for situations where the specification does not indicate required chain constraints. You should generally
+ /// try to use the none `Any` version in preference.
+ ///
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static ManagedChain DuplicateAny(this ManagedChain chain)
+ where TChain : struct, IChainable
+ where T1 : struct, IChainable
+ {
+ var size = ManagedChain.MemorySize;
+ var newHeadPtr = Marshal.AllocHGlobal(size);
+ // Block copy original struct data for speed
+ System.Buffer.MemoryCopy(chain.HeadPtr, (void*)newHeadPtr, size, size);
+ // Update all pointers
+ ((BaseInStructure*)newHeadPtr)->PNext = (BaseInStructure*) (newHeadPtr + ManagedChain.Item1Offset);
+ return new ManagedChain(newHeadPtr);
+ }
+
+ ///
+ /// Creates a new with 1 items, by removing the last item
+ /// from the .
+ ///
+ /// The chain.
+ /// The chain type
+ /// Type of Item 1.
+ ///
+ /// Do not forget to dispose this chain if you are no longer using it.
+ ///
+ ///
+ ///
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static ManagedChain Truncate(this ManagedChain chain)
+ where TChain : struct, IChainStart
+ where T1 : struct, IExtendsChain
+ => chain.TruncateAny(out var _);
+
+ ///
+ /// Creates a new with 1 items, by removing the last item
+ /// from the .
+ ///
+ /// The chain.
+ /// The chain type
+ /// Type of Item 1.
+ ///
+ /// Do not forget to dispose this chain if you are no longer using it.
+ /// The `Any` versions of chain methods do not validate that items belong in the chain, this is
+ /// useful for situations where the specification does not indicate required chain constraints. You should generally
+ /// try to use the none `Any` version in preference.
+ ///
+ ///
+ ///
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static ManagedChain TruncateAny(this ManagedChain chain)
+ where TChain : struct, IChainable
+ where T1 : struct, IChainable
+ => chain.TruncateAny(out var _);
+
+ ///
+ /// Creates a new with 1 items, by removing
+ /// from the end of the .
+ ///
+ /// The chain.
+ /// The item removed from the .
+ /// The chain type
+ /// Type of Item 1.
+ ///
+ /// Do not forget to dispose this chain if you are no longer using it.
+ ///
+ ///
+ ///
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static ManagedChain Truncate(this ManagedChain chain, out T1 item1)
+ where TChain : struct, IChainStart
+ where T1 : struct, IExtendsChain
+ => chain.TruncateAny(out item1);
+
+ ///
+ /// Creates a new with 1 items, by removing
+ /// from the end of the .
+ ///
+ /// The chain.
+ /// The item removed from the .
+ /// The chain type
+ /// Type of Item 1.
+ ///
+ /// Do not forget to dispose this chain if you are no longer using it.
+ /// The `Any` versions of chain methods do not validate that items belong in the chain, this is
+ /// useful for situations where the specification does not indicate required chain constraints. You should generally
+ /// try to use the none `Any` version in preference.
+ ///
+ ///
+ ///
+ ///
+ public static ManagedChain TruncateAny(this ManagedChain chain, out T1 item1)
+ where TChain : struct, IChainable
+ where T1 : struct, IChainable
+ {
+ // Retrieve last item.
+ item1 = chain.Item1;
+
+ var newSize = ManagedChain.MemorySize - ManagedChain.Item1Size;
+ var newHeadPtr = Marshal.AllocHGlobal(newSize);
+ // Block copy original struct data for speed
+ System.Buffer.MemoryCopy(chain.HeadPtr, (void*)newHeadPtr, newSize, newSize);
+ // Update all pointers
+ ((BaseInStructure*)newHeadPtr)->PNext = null;
+ return new ManagedChain(newHeadPtr);
+ }
+
+ ...
+}
+```
\ No newline at end of file
diff --git a/documentation/proposals/Proposal - Vulkan Struct Chaining - #4 Chaining Metadata extensions.md b/documentation/proposals/Proposal - Vulkan Struct Chaining - #4 Chaining Metadata extensions.md
new file mode 100644
index 0000000000..5a1153fcfb
--- /dev/null
+++ b/documentation/proposals/Proposal - Vulkan Struct Chaining - #4 Chaining Metadata extensions.md
@@ -0,0 +1,393 @@
+# Summary
+
+**_This proposal is dependent
+on [Proposal - Vulkan Struct Chaining - #2 Unmanaged Chaining](Proposal%20-%20Vulkan%20Struct%20Chaining%20-%20%232%20Unmanaged%20Chaining.md)
+._**
+
+Having extended `BuildTools` to make use of the `structextends` attribute, as part of
+the [unmanaged chaining proposal](Proposal%20-%20Vulkan%20Struct%20Chaining%20-%20%232%20Unmanaged%20Chaining.md)
+it is possible to supply that metadata in a ready form for use by consumers of the library, without them needing to
+resort to reflection.
+
+There are a number of potential use cases, most specifically validating that a supply `IChainable` structure is valid
+for a given chain, at runtime. To facilitate the most common scenario 4 extension methods are also added.
+
+# Contributors
+
+- [Craig Dean, DevDecoder](https://github.com/thargy)
+
+# Current Status
+
+- [x] Proposed
+- [ ] Discussed with API Review Board (ARB)
+- [ ] Approved
+- [ ] Implemented
+
+# Design Decisions
+
+- It is possible to get the same information presented statically by this proposal by creative use of reflection,
+ however, it is non-trivial and costly to do so.
+- When combined with the
+ [Proposal - Vulkan Struct Chaining - #3 Managed Chaining](Proposal%20-%20Vulkan%20Struct%20Chaining%20-%20%233%20Managed%20Chaining.md)
+ the above functionality makes handling of non-generic chains at runtime significantly easier, which is great for
+ library writers.
+- Though 4 collections are proposed, they are grouped in pairs, providing two-way mapping. It is possible to only
+ provide one of each pair, and leave the reversal to the consumer, though that would make the extension methods
+ impractical.
+- As .Net Standard 2.0 does not support `IReadOnlySet` we make use of the `IReadOnlyCollection` interface.
+
+# Proposed API
+
+## Auto-generated Metadata Structures
+
+The `Extensions` dictionary is added to the `Chain` extensions class from
+the [unmanaged chaining proposal](Proposal%20-%20Vulkan%20Struct%20Chaining%20-%20%232%20Unmanaged%20Chaining.md)
+for discoverability. It is a direct mapping of the `structextends` attribute, and is therefore trivial to generate.
+Below is a cut down example to illustrate what will be generated:
+
+```csharp
+public static class Chain : IChainable
+{
+ ...
+ ///
+ /// Provides a set of all the s that can be extended by a .
+ ///
+ public static readonly IReadOnlyDictionary> Extensions =
+ new Dictionary>
+ {
+ [StructureType.PhysicalDeviceFeatures2] = new HashSet
+ {
+ StructureType.DeviceCreateInfo
+ },
+ [StructureType.PhysicalDeviceDescriptorIndexingFeatures] = new HashSet
+ {
+ StructureType.PhysicalDeviceFeatures2,
+ StructureType.DeviceCreateInfo
+ },
+ [StructureType.PhysicalDeviceAccelerationStructureFeaturesKhr] = new HashSet
+ {
+ StructureType.PhysicalDeviceFeatures2,
+ StructureType.DeviceCreateInfo
+ },
+ ...
+ };
+}
+```
+
+## Extenders (Optional)
+
+By contrast the `Extenders` dictionary, it the reverse mapping of the `Extensions` dictionary, providing quick reverse
+lookup. As this is slightly more complex to generate, it could be left to the consumer (who can generate it easily
+from `Extensions` when needed).
+
+```csharp
+public struct Chain : IChainable
+{
+ ...
+ ///
+ /// Provides a set of all the s that can extend a .
+ ///
+ public static readonly IReadOnlyDictionary> Extenders =
+ new Dictionary>
+ {
+ [StructureType.DeviceCreateInfo] = new HashSet
+ {
+ StructureType.PhysicalDeviceFeatures2, StructureType.PhysicalDeviceDescriptorIndexingFeatures,
+ StructureType.PhysicalDeviceAccelerationStructureFeaturesKhr
+ },
+ [StructureType.PhysicalDeviceFeatures2] = new HashSet
+ {
+ StructureType.PhysicalDeviceDescriptorIndexingFeatures,
+ StructureType.PhysicalDeviceAccelerationStructureFeaturesKhr
+ },
+ ...
+ };
+}
+```
+
+### ClrTypes
+
+The `ClrTypes` dictionary provides a `ClrType` for each `StructureType`, it is trivial to generate with existing
+information:
+
+```csharp
+public struct Chain : IChainable
+{
+ ...
+ ///
+ /// Provides a mapping from the to the corresponding .
+ ///
+ public static readonly IReadOnlyDictionary ClrTypes =
+ new Dictionary
+ {
+ [StructureType.DeviceCreateInfo] = typeof(DeviceCreateInfo),
+ [StructureType.PhysicalDeviceFeatures2] = typeof(PhysicalDeviceFeatures2),
+ [StructureType.PhysicalDeviceDescriptorIndexingFeatures]= typeof(PhysicalDeviceDescriptorIndexingFeatures),
+ [StructureType.PhysicalDeviceAccelerationStructureFeaturesKhr] = typeof(PhysicalDeviceAccelerationStructureFeaturesKhr),
+ ...
+ };
+}
+```
+
+### StructureTypes (Optional)
+
+The `StructureTypes` is the reverse mapping of `ClrTypes` and is likewise trivial to generate.
+
+```csharp
+public struct Chain : IChainable
+{
+ ...
+ ///
+ /// Provides a mapping from the to the corresponding .
+ ///
+ public static readonly IReadOnlyDictionary StructureTypes =
+ new Dictionary
+ {
+ [typeof(DeviceCreateInfo)] = StructureType.DeviceCreateInfo,
+ [typeof(PhysicalDeviceFeatures2)] = StructureType.PhysicalDeviceFeatures2,
+ [typeof(PhysicalDeviceDescriptorIndexingFeatures)]= StructureType.PhysicalDeviceDescriptorIndexingFeatures,
+ [typeof(PhysicalDeviceAccelerationStructureFeaturesKhr)] = StructureType.PhysicalDeviceAccelerationStructureFeaturesKhr
+ ...
+ };
+}
+```
+
+## Extension Methods
+
+The following extension methods are not auto-generated and so can be added simply.
+
+### ClrType()
+
+Gets the corresponding `ClrType` for a `StructureTYpe`.
+
+```csharp
+public static class Chain
+{
+ ...
+ ///
+ /// Gets the corresponding for a , if any.
+ ///
+ /// The structure type.
+ /// The corresponding for , if any; otherwise,
+ /// .
+ public static Type ClrType(this StructureType structureType)
+ {
+ return Chain.ClrTypes[structureType];
+ }
+}
+```
+
+Usage:
+
+```csharp
+Assert.Equal(typeof(DeviceCreateInfo), StructureType.DeviceCreateInfo.ClrType());
+```
+
+### StructureType() (Optional)
+
+Gets the corresponding `ClrType` for a `StructureTYpe`.
+
+```csharp
+public static class Chain
+{
+ ...
+ ///
+ /// Gets the corresponding for a , if any.
+ ///
+ /// The CLR type.
+ /// The corresponding for , if any; otherwise,
+ /// .
+ public static StructureType? StructureType(this Type type)
+ {
+ return Chain.StructureTypes.TryGetValue(type, out var structureType) ? structureType : null;
+ }
+}
+```
+
+Usage:
+
+```csharp
+Assert.Equal(StructureType.DeviceCreateInfo, typeof(DeviceCreateInfo).StructureType());
+Assert.Null(typeof(PhysicalDeviceFeatures).StructureType());
+```
+
+### IsChainStart()
+
+Tests whether the `StructureType` or `Type` can be used at the start of a chain:
+
+```csharp
+public static class Chain
+{
+ ...
+ ///
+ /// Whether the can start a chain.
+ ///
+ /// The to test.
+ /// if the can start a chain; otherwise
+ /// .
+ public static bool IsChainStart(this StructureType type)
+ {
+ return Chain.Extenders.ContainsKey(type);
+ }
+
+ ///
+ /// Whether the can start a chain.
+ ///
+ /// The to test.
+ /// if the can start a chain; otherwise
+ /// .
+ public static bool IsChainStart(this Type type)
+ {
+ return Chain.StructureTypes.TryGetValue(type, out var structureType) &&
+ Chain.Extenders.ContainsKey(structureType);
+ }
+}
+```
+
+Usage:
+
+```csharp
+Assert.True(StructureType.DeviceCreateInfo.IsChainStart());
+Assert.True(typeof(DeviceCreateInfo).IsChainStart());
+Assert.False(StructureType.PhysicalDeviceDescriptorIndexingFeatures.IsChainStart());
+Assert.False(typeof(PhysicalDeviceDescriptorIndexingFeatures).IsChainStart());
+```
+
+### IsChainable()
+
+Tests whether the `StructureType` or `Type` can be used in a chain:
+
+```csharp
+public static class Chain
+{
+ ...
+ ///
+ /// Whether the is chainable.
+ ///
+ /// The to test.
+ /// if the can start a chain; otherwise
+ /// .
+ public static bool IsChainable(this StructureType type)
+ {
+ return Chain.Extenders.ContainsKey(type) ||
+ Chain.Extensions.ContainsKey(type);
+ }
+
+ ///
+ /// Whether the is chainable.
+ ///
+ /// The to test.
+ /// if the can start a chain; otherwise
+ /// .
+ public static bool IsChainable(this Type type)
+ {
+ return Chain.StructureTypes.TryGetValue(type, out var structureType) &&
+ (Chain.Extenders.ContainsKey(structureType) || Chain.Extensions.ContainsKey(structureType));
+ }
+}
+```
+
+Usage:
+
+```csharp
+Assert.True(StructureType.DeviceCreateInfo.IsChainable());
+Assert.True(typeof(DeviceCreateInfo).IsChainable());
+Assert.True(StructureType.PhysicalDeviceDescriptorIndexingFeatures.IsChainable());
+Assert.True(typeof(PhysicalDeviceDescriptorIndexingFeatures).IsChainable());
+```
+
+### CanExtend()
+
+Tests whether the `StructureType` or `Type` can extend the corresponding type:
+
+```csharp
+public static class Chain
+{
+ ...
+
+ ///
+ /// Whether the current can extend the .
+ ///
+ /// The to test.
+ /// The of the chain.
+ /// if the can extend the ; otherwise, false.
+ ///
+ public static bool CanExtend(this StructureType next, StructureType chain)
+ {
+ return Chain.Extensions.TryGetValue(next, out var extensions) && extensions.Contains(chain);
+ }
+
+ ///
+ /// Whether the current can extend the .
+ ///
+ /// The to test.
+ /// The of the chain.
+ /// if the can extend the ; otherwise, false.
+ ///
+ public static bool CanExtend(this Type next, Type chain)
+ {
+ return
+ Chain.StructureTypes.TryGetValue(next, out var nextType) &&
+ Chain.StructureTypes.TryGetValue(chain, out var chainType) &&
+ Chain.Extensions.TryGetValue(nextType, out var extensions) &&
+ extensions.Contains(chainType);
+ }
+}
+```
+
+Usage:
+
+```csharp
+Assert.True(StructureType.PhysicalDeviceFeatures2.CanExtend(StructureType.DeviceCreateInfo));
+Assert.False(StructureType.DeviceCreateInfo.CanExtend(StructureType.PhysicalDeviceFeatures2));
+Assert.True(typeof(PhysicalDeviceFeatures2).CanExtend(typeof(DeviceCreateInfo)));
+Assert.False(typeof(DeviceCreateInfo).CanExtend(typeof(PhysicalDeviceFeatures2)));
+```
+
+### CanBeExtendedBy()
+
+Tests whether the `StructureType` or `Type` can be extended by the corresponding type:
+
+```csharp
+public static class Chain
+{
+ ...
+
+ ///
+ /// Whether the current can be extended by the .
+ ///
+ /// The of the chain.
+ /// The to test.
+ /// if the can be extended the ; otherwise, false.
+ ///
+ public static bool CanBeExtendedBy(this StructureType chain, StructureType next)
+ {
+ return Chain.Extenders.TryGetValue(chain, out var extenders) && extenders.Contains(next);
+ }
+
+ ///
+ /// Whether the current can be extended by the .
+ ///
+ /// The of the chain.
+ /// The to test.
+ /// if the can extend the ; otherwise, false.
+ ///
+ public static bool CanBeExtendedBy(this Type chain, Type next)
+ {
+ return
+ Chain.StructureTypes.TryGetValue(next, out var nextType) &&
+ Chain.StructureTypes.TryGetValue(chain, out var chainType) &&
+ Chain.Extenders.TryGetValue(chainType, out var extenders) &&
+ extenders.Contains(nextType);
+ }
+}
+```
+
+Usage:
+
+```csharp
+Assert.False(StructureType.PhysicalDeviceFeatures2.CanBeExtendedBy(StructureType.DeviceCreateInfo));
+Assert.True(StructureType.DeviceCreateInfo.CanBeExtendedBy(StructureType.PhysicalDeviceFeatures2));
+Assert.False(typeof(PhysicalDeviceFeatures2).CanBeExtendedBy(typeof(DeviceCreateInfo)));
+Assert.True(typeof(DeviceCreateInfo).CanBeExtendedBy(typeof(PhysicalDeviceFeatures2)));
+```
\ No newline at end of file
diff --git a/src/Core/Silk.NET.BuildTools/Bind/ProjectWriter.cs b/src/Core/Silk.NET.BuildTools/Bind/ProjectWriter.cs
index 24932a3493..bb2f589938 100644
--- a/src/Core/Silk.NET.BuildTools/Bind/ProjectWriter.cs
+++ b/src/Core/Silk.NET.BuildTools/Bind/ProjectWriter.cs
@@ -1,6 +1,8 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+using System;
+using System.Collections.Generic;
using System.IO;
using System.Linq;
using Silk.NET.BuildTools.Common;
@@ -9,6 +11,30 @@ namespace Silk.NET.BuildTools.Bind
{
public static class ProjectWriter
{
+ ///
+ /// Creates a unique file name (that doesn't already exist), preventing clashes of names with different cases on
+ /// case insensitive filesystems.
+ ///
+ /// The name (goes before any disambiguator)
+ /// The extension (goes after any disambiguator)
+ /// Any number of directory segements
+ /// A unique filename.
+ private static string GetFileName(string name, string ext, params string[] directories)
+ {
+ var count = 1;
+ string filename;
+ Array.Resize(ref directories, directories.Length + 1);
+ var nameIndex = directories.Length - 1;
+ do
+ {
+ directories[nameIndex] = $"{name}{(count > 1 ? count.ToString() : "")}{ext}";
+ filename = Path.Combine(directories);
+ count++;
+ } while (File.Exists(filename));
+
+ return filename;
+ }
+
///
/// Writes this project in the given folder, with the given settings and parent subsystem.
///
@@ -38,14 +64,16 @@ public static void Write(this Project project, string folder, Profile profile, B
(
x => x.WriteStruct
(
- Path.Combine(folder, ProfileWriter.StructsSubfolder, $"{x.Name}.gen.cs"), profile, project, task
+ GetFileName(x.Name, ".gen.cs", folder, ProfileWriter.StructsSubfolder), profile, project, task
)
);
project.Enums.ForEach
(
x => x.WriteEnum
- (Path.Combine(folder, ProfileWriter.EnumsSubfolder, $"{x.Name}.gen.cs"), profile, project, task)
+ (
+ GetFileName(x.Name, ".gen.cs", folder, ProfileWriter.EnumsSubfolder), profile, project, task
+ )
);
project.WriteMixedModeClasses(profile, folder, task);
diff --git a/src/Core/Silk.NET.BuildTools/Bind/StructWriter.cs b/src/Core/Silk.NET.BuildTools/Bind/StructWriter.cs
index 4d5c5b8f1b..68b595d906 100644
--- a/src/Core/Silk.NET.BuildTools/Bind/StructWriter.cs
+++ b/src/Core/Silk.NET.BuildTools/Bind/StructWriter.cs
@@ -1,4 +1,4 @@
-// Licensed to the .NET Foundation under one or more agreements.
+// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
@@ -27,10 +27,11 @@ public static class StructWriter
/// The file to write to.
/// The subsystem containing this enum.
/// The project containing this enum.
+ /// The bind state.
public static void WriteStruct
(this Struct @struct, string file, Profile profile, Project project, BindState task)
{
- if (@struct.Attributes.IsBuildToolsIntrinsic(out var args))
+ if (@struct.Attributes.IsBuildToolsIntrinsic(out var args) && args.FirstOrDefault() == "$PFN")
{
WriteBuildToolsIntrinsic(@struct, file, profile, project, task, args);
return;
@@ -47,14 +48,53 @@ public static void WriteStruct
sw.WriteLine($"namespace {ns}{project.Namespace}");
sw.WriteLine("{");
string guid = null;
-
+
static bool IsChar(Type type) => type.Name == "char" || type.GenericTypes.Any(IsChar);
var needsCharSetFixup = @struct.Fields.Any(x => IsChar(x.Type));
-
+ string structuredType = null;
+ var isChainable = false;
+ // Which chain this struct extends
+ IReadOnlyList chainExtensions = null;
+ // Which chain extend the current struct
+ IReadOnlyList chainExtenderss = null;
+ IReadOnlyList aliases = null;
+ string aliasOf = null;
+
foreach (var attr in @struct.Attributes)
{
if (attr.Name == "BuildToolsIntrinsic")
{
+ if (attr.Arguments.Count > 0)
+ {
+ switch (attr.Arguments[0])
+ {
+ case "$VKSTRUCTUREDTYPE":
+ structuredType = attr.Arguments.Count > 1 ? attr.Arguments[1] : null;
+ break;
+ case "$VKCHAINABLE":
+ isChainable = true;
+ break;
+ case "$VKEXTENDSCHAIN":
+ chainExtensions = attr.Arguments.Count > 1 ? attr.Arguments.Skip(1).ToArray() : null;
+ break;
+ case "$VKCHAINSTART":
+ chainExtenderss = attr.Arguments.Count > 1 ? attr.Arguments.Skip(1).ToArray() : null;
+ break;
+ case "$VKALIASOF":
+ aliasOf = attr.Arguments.Count > 1 ? attr.Arguments[1] : null;
+ break;
+ case "$VKALIASES":
+ aliases = attr.Arguments.Count > 1 ? attr.Arguments.Skip(1).ToArray() : null;
+ break;
+ default:
+ Console.WriteLine
+ (
+ $"Unexpected build intrinsic attribute '{attr.Arguments[0]}' on '{@struct.Name}' struct!"
+ );
+ break;
+ }
+ }
+
continue;
}
@@ -72,13 +112,51 @@ public static void WriteStruct
sw.WriteLine($" {attr}");
}
+ // Build interface list
+ var interfaces = new List();
+ if (chainExtenderss?.Any() == true)
+ {
+ interfaces.Add("IChainStart");
+ }
+
+ interfaces.AddRange
+ (
+ chainExtensions?.Select(e => $"IExtendsChain<{e}>") ??
+ Array.Empty()
+ );
+ if (!interfaces.Any())
+ {
+ // We only need to add these interfaces if a descendant not already added above.
+ if (isChainable)
+ {
+ interfaces.Add("IChainable");
+ }
+ else if (structuredType is not null)
+ {
+ interfaces.Add("IStructuredType");
+ }
+ }
+
if (needsCharSetFixup)
{
sw.WriteLine(" [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]");
}
sw.WriteLine($" [NativeName(\"Name\", \"{@struct.NativeName}\")]");
- sw.WriteLine($" public unsafe partial struct {@struct.Name}");
+ if (!string.IsNullOrWhiteSpace(aliasOf))
+ {
+ sw.WriteLine($" [NativeName(\"AliasOf\", \"{aliasOf}\")]");
+ }
+
+ if (aliases is not null)
+ {
+ sw.WriteLine($" [NativeName(\"Aliases\", \"{string.Join(", ", aliases)}\")]");
+ }
+
+ sw.WriteLine
+ (
+ $" public unsafe partial struct {@struct.Name}{(interfaces.Any() ? " : " + string.Join(", ", interfaces) : string.Empty)}"
+ );
sw.WriteLine(" {");
if (guid is not null)
{
@@ -214,7 +292,8 @@ public static void WriteStruct
}
else if (structField.NumBits is not null)
{
- WriteBitfield(structField, ref bitfieldIdx, ref bitfieldPsz, ref bitfieldRbs, ref bitfieldLbt, sw, profile);
+ WriteBitfield
+ (structField, ref bitfieldIdx, ref bitfieldPsz, ref bitfieldRbs, ref bitfieldLbt, sw, profile);
}
else if (!(structField.Count is null))
{
@@ -347,7 +426,7 @@ public static void WriteStruct
}
foreach (var function in @struct.Functions.Concat
- (ComVtblProcessor.GetHelperFunctions(@struct, profile.Projects["Core"])))
+ (ComVtblProcessor.GetHelperFunctions(@struct, profile.Projects["Core"])))
{
using (var sr = new StringReader(function.Signature.Doc))
{
@@ -364,7 +443,7 @@ public static void WriteStruct
}
using (var sr = new StringReader
- (function.Signature.ToString(null, accessibility: true, semicolon: false)))
+ (function.Signature.ToString(null, accessibility: true, semicolon: false)))
{
string line;
while ((line = sr.ReadLine()) != null)
@@ -383,6 +462,89 @@ public static void WriteStruct
sw.WriteLine();
}
+ if (structuredType is not null)
+ {
+ sw.WriteLine
+ (
+ @"
+ /// "
+ );
+ if (structuredType.Length < 1)
+ {
+ sw.WriteLine(" /// Note, there is no fixed value for this type.");
+ }
+
+ sw.Write
+ (
+ @" StructureType IStructuredType.StructureType()
+ {
+ return SType"
+ );
+ if (structuredType.Length > 0)
+ {
+ sw.Write(" = ");
+ sw.Write(structuredType);
+ }
+
+ sw.WriteLine
+ (
+ @";
+ }"
+ );
+
+
+ if (isChainable)
+ {
+ // Correct for none void* or BaseInStructure* PNext
+ var pNextType = @struct.Fields.FirstOrDefault(f => f.Name == "PNext")?.Type.Name ?? "void";
+ string getCast, setCast;
+ switch (pNextType)
+ {
+ case "void":
+ getCast = "(BaseInStructure*) ";
+ setCast = "";
+ break;
+ case "BaseInStructure":
+ getCast = setCast = "";
+ break;
+ default:
+ getCast = "(BaseInStructure*) ";
+ setCast = $"({pNextType}*) ";
+ break;
+
+ }
+ sw.WriteLine
+ (
+ @$"
+ ///
+ unsafe BaseInStructure* IChainable.PNext
+ {{
+ get => {getCast}PNext;
+ set => PNext = {setCast}value;
+ }}"
+ );
+ }
+
+ if (chainExtenderss?.Any() == true && structuredType.Length > 0)
+ {
+ sw.WriteLine
+ (
+ @$"
+ ///
+ /// Convenience method to start a chain.
+ ///
+ /// The newly created chain root
+ /// A reference to the newly created chain.
+ public static unsafe ref {@struct.Name} Chain(
+ out {@struct.Name} capture)
+ {{
+ capture = new {@struct.Name}({structuredType});
+ return ref capture;
+ }}"
+ );
+ }
+ }
+
sw.WriteLine(" }");
sw.WriteLine("}");
sw.Flush();
@@ -395,7 +557,9 @@ public static void WriteBuildToolsIntrinsic
if (args[0] == "$PFN")
{
WriteFunctionPointerWrapper
- (@struct, file, profile, project, task, args[1], args[2], Enum.Parse(args[3]));
+ (
+ @struct, file, profile, project, task, args[1], args[2], Enum.Parse(args[3])
+ );
}
else
{
@@ -468,7 +632,8 @@ CallingConvention conv
sw.WriteLine($" => SilkMarshal.PtrToDelegate<{delegateName}>(pfn);");
sw.WriteLine();
sw.WriteLine($" public static implicit operator {fnPtrSig}({pfnName} pfn) => pfn.Handle;");
- sw.WriteLine($" public static implicit operator {pfnName}({fnPtrSig} ptr) => new {pfnName}(ptr);");
+ sw.WriteLine
+ ($" public static implicit operator {pfnName}({fnPtrSig} ptr) => new {pfnName}(ptr);");
sw.WriteLine(" }");
sw.WriteLine();
// type.FunctionPointerSignature.Name = delegateName;
@@ -476,7 +641,10 @@ CallingConvention conv
// type.Name = type.FunctionPointerSignature.NativeName;
// type.IndirectionLevels--;
using (var sr = new StringReader
- (type.FunctionPointerSignature.ToString(null, @delegate: true, semicolon: true, accessibility: true)))
+ (
+ type.FunctionPointerSignature.ToString
+ (null, @delegate: true, semicolon: true, accessibility: true)
+ ))
{
string line;
while ((line = sr.ReadLine()) != null)
@@ -490,7 +658,8 @@ CallingConvention conv
sw.Flush();
}
- public static void WriteFusedField(Struct @struct, Project p, Field field, List args, StreamWriter sw)
+ public static void WriteFusedField
+ (Struct @struct, Project p, Field field, List args, StreamWriter sw)
{
var temporaryValue = IsTemporaryValue(p, @struct, args);
if (!temporaryValue)
@@ -519,7 +688,8 @@ public static void WriteFusedField(Struct @struct, Project p, Field field, List<
static bool IsTemporaryValue(Project p, Struct @struct, List args)
{
// ReSharper disable AccessToModifiedClosure
- var fusingStruct = p.Structs.First(x => x.Name == @struct.Fields.First(y => y.Name == args[1]).Type.Name);
+ var fusingStruct = p.Structs.First
+ (x => x.Name == @struct.Fields.First(y => y.Name == args[1]).Type.Name);
var fusingFieldInst = fusingStruct.Fields.First(x => x.Name == args[2]);
if (fusingFieldInst.NumBits is not null)
{
@@ -609,7 +779,8 @@ Profile profile
}
var bitfieldOffset = currentSize * 8 - remainingBits;
- var bitwidthHexStringBacking = ((1 << fieldDecl.NumBits.Value) - 1).ToString("X") + typeNameBacking switch
+ var bitwidthHexStringBacking = ((1 << fieldDecl.NumBits.Value) - 1).ToString
+ ("X") + typeNameBacking switch
{
"byte" => string.Empty,
"sbyte" => string.Empty,
diff --git a/src/Core/Silk.NET.BuildTools/Common/Attribute.cs b/src/Core/Silk.NET.BuildTools/Common/Attribute.cs
index 377d89b047..aa78182f00 100644
--- a/src/Core/Silk.NET.BuildTools/Common/Attribute.cs
+++ b/src/Core/Silk.NET.BuildTools/Common/Attribute.cs
@@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System.Collections.Generic;
+using System.Linq;
using System.Text;
namespace Silk.NET.BuildTools.Common
@@ -39,5 +40,18 @@ public override string ToString()
sb.Append(")]");
return sb.ToString();
}
+
+ ///
+ /// Clones this attribute.
+ ///
+ /// A copy.
+ public Attribute Clone()
+ {
+ return new Attribute
+ {
+ Name = Name,
+ Arguments = Arguments.ToList()
+ };
+ }
}
}
diff --git a/src/Core/Silk.NET.BuildTools/Common/Struct.cs b/src/Core/Silk.NET.BuildTools/Common/Struct.cs
index a9d643963c..6223bcf846 100644
--- a/src/Core/Silk.NET.BuildTools/Common/Struct.cs
+++ b/src/Core/Silk.NET.BuildTools/Common/Struct.cs
@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
+using System.Linq;
using Newtonsoft.Json;
using Silk.NET.BuildTools.Common.Functions;
using Silk.NET.BuildTools.Common.Structs;
@@ -18,30 +19,31 @@ public class Struct : IProfileConstituent
/// The name of the struct.
///
public string Name { get; set; }
-
+
///
/// The name of the struct in the native library.
///
public string NativeName { get; set; }
-
+
///
/// The extension name.
///
public string ExtensionName { get; set; }
+
[JsonIgnore] public string ProfileName { get; set; }
[JsonIgnore] public Version ProfileVersion { get; set; }
[JsonIgnore] public string[] ClangMetadata { get; set; }
-
+
///
/// A list of fields this struct contains.
///
public List Fields { get; set; } = new List();
-
+
///
/// A list of attributes this struct has.
///
public List Attributes { get; set; } = new List();
-
+
///
/// A list of functions this struct has.
///
@@ -51,7 +53,7 @@ public class Struct : IProfileConstituent
/// A list of functions contained in the LpVtbl field.
///
public List Vtbl { get; set; } = new List();
-
+
///
/// A list of interface names which this interface inherits.
///
@@ -61,5 +63,34 @@ public class Struct : IProfileConstituent
/// This struct's UUID attribute.
///
public Guid? Uuid { get; set; }
+
+ ///
+ /// Creates an alias of this structure, by cloning it.
+ ///
+ /// The alias name.
+ /// A copy of this instance.
+ public Struct Clone(string alias = null, string nativeAlias = null)
+ {
+ return new Struct
+ {
+ Name = alias ?? Name,
+ // These properties can simply be copied.
+ NativeName = nativeAlias ?? NativeName,
+ ExtensionName = ExtensionName,
+ ProfileName = ProfileName,
+ ProfileVersion = ProfileVersion,
+ Uuid = Uuid,
+ // WARNING these aren't deep clones so modifying any will modify the alias
+ // This is by design as an alias should not differ in any of these ways, and it means
+ // that modifying the source or the alias will affect both, which is desirable.
+ ClangMetadata = ClangMetadata,
+ Fields = Fields,
+ Functions = Functions,
+ Vtbl = Vtbl,
+ ComBases = ComBases,
+ // Deep clone, as we often want different attributes on an alias
+ Attributes = Attributes.Select(a => a.Clone()).ToList()
+ };
+ }
}
-}
\ No newline at end of file
+}
diff --git a/src/Core/Silk.NET.BuildTools/Converters/Khronos/StructureDefinition.cs b/src/Core/Silk.NET.BuildTools/Converters/Khronos/StructureDefinition.cs
index 1de1946f5d..af3f9cae25 100644
--- a/src/Core/Silk.NET.BuildTools/Converters/Khronos/StructureDefinition.cs
+++ b/src/Core/Silk.NET.BuildTools/Converters/Khronos/StructureDefinition.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
@@ -7,39 +8,57 @@ namespace Silk.NET.BuildTools.Converters.Khronos
public class StructureDefinition
{
public string Name { get; }
+
+ ///
+ /// The alias holds the concrete structure definition's name.
+ ///
+ public string Alias { get; }
public MemberSpec[] Members { get; }
+ public IReadOnlyList Extends { get; }
- public StructureDefinition(string name, MemberSpec[] members)
+ public StructureDefinition(string name, string alias, MemberSpec[] members, string extends)
{
Name = name;
+ Alias = alias;
Members = members;
+ Extends = string.IsNullOrWhiteSpace(extends)
+ ? Array.Empty()
+ : extends.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
}
public static StructureDefinition CreateFromXml(XElement xe)
{
Require.NotNull(xe);
+ var name = xe.GetNameAttribute();
- if (!(xe.Attribute("alias") is null))
+ // Find original definition if this is an alias (this should usually be one step, but we handle gracefully)
+ var aliases = new HashSet { name };
+ string alias = null;
+ string a;
+ while ((a = xe.Attribute("alias")?.Value) is not null)
{
- var ret = CreateFromXml
- (
- xe.Document.Element("registry")
- .Elements("types")
- .Elements("type")
- .Where(typex => typex.HasCategoryAttribute("struct"))
- .FirstOrDefault(x => x.GetNameAttribute() == xe.Attribute("alias").Value) ?? throw new Exception("wat")
- );
-
- return new StructureDefinition(xe.GetNameAttribute(), ret.Members);
+ alias = a;
+ if (aliases.Contains(alias))
+ {
+ throw new Exception($"A loop of aliases was found as we have seen the '{alias}' alias before.");
+ }
+ aliases.Add(alias);
+
+ xe = xe.Document.Element("registry")
+ .Elements("types")
+ .Elements("type")
+ .Where(typex => typex.HasCategoryAttribute("struct"))
+ .FirstOrDefault(x => x.GetNameAttribute() == alias) ??
+ throw new Exception($"Could not find the '{alias}' alias.");
}
- var name = xe.GetNameAttribute();
var members = xe.Elements
("member")
.Where(x => !(x.Element("name") is null))
.Select(memberx => MemberSpec.CreateFromXml(memberx))
.ToArray();
- return new StructureDefinition(name, members);
+ var extends = xe.Attribute("structextends")?.Value;
+ return new StructureDefinition(name, alias, members, extends);
}
public override string ToString()
diff --git a/src/Core/Silk.NET.BuildTools/Converters/Readers/VulkanReader.cs b/src/Core/Silk.NET.BuildTools/Converters/Readers/VulkanReader.cs
index 923bdf914a..54198304d6 100644
--- a/src/Core/Silk.NET.BuildTools/Converters/Readers/VulkanReader.cs
+++ b/src/Core/Silk.NET.BuildTools/Converters/Readers/VulkanReader.cs
@@ -55,47 +55,246 @@ private Dictionary ConvertStructs(VulkanSpecification spec, Bind
{
var prefix = task.FunctionPrefix;
var ret = new Dictionary();
+
+ // Gets all aliases of a struct, no matter where in an alias chain we start
+ // Note this could be simpler if we just assume we only need to check $VKALIASES, but this
+ // version is bombproof.
+ IReadOnlyList GetAllAliasesFromName(string structName)
+ {
+ var todo = new Queue();
+ todo.Enqueue(structName);
+ var result = new Dictionary();
+
+ while (todo.Any())
+ {
+ structName = todo.Dequeue();
+ if (!ret.TryGetValue(structName, out var s))
+ {
+ result[structName] = null;
+ continue;
+ }
+
+ result[structName] = s;
+
+ // Get any aliases
+ var aliasOf = s.Attributes
+ .FirstOrDefault
+ (
+ a => a.Arguments.Count > 1 && a.Name == "BuildToolsIntrinsic" &&
+ a.Arguments[0] == "$VKALIASOF"
+ )
+ ?.Arguments[1];
+ if (!string.IsNullOrWhiteSpace(aliasOf) && !result.ContainsKey(aliasOf))
+ {
+ todo.Enqueue(aliasOf);
+ }
+
+ // Check other way as well
+ foreach (var a in s.Attributes
+ .FirstOrDefault
+ (
+ a => a.Arguments.Count > 1 && a.Name == "BuildToolsIntrinsic" &&
+ a.Arguments[0] == "$VKALIASES"
+ )
+ ?.Arguments
+ .Skip(1)
+ .Where(a => !string.IsNullOrWhiteSpace(a) && !result.ContainsKey(a))
+ .ToArray()
+ ?? Array.Empty())
+ {
+ todo.Enqueue(a);
+ }
+ }
+
+ return result.Values.Where(s => s is not null).ToList();
+ }
+
+ // Opposite way round lookup of what aliases exist for this key-struct
+ // i.e. if VkB is an alias of VkA, and VkC is an alias of VkA, then aliases has [VkA]={VkB,VkC}
+ var aliases = new Dictionary>();
+ // Holds any chain starts for chains we haven't seen yet (should rarely be needed).
+ var chainExtensions = new List<(Struct, IReadOnlyList)>();
foreach (var s in spec.Structures)
{
- ret.Add
- (
- s.Name, new Struct
+ // Build aliases dictionary
+ if (!string.IsNullOrWhiteSpace(s.Alias))
+ {
+ if (!aliases.TryGetValue(s.Alias, out var aList))
{
- Fields = s.Members.Select
- (
- x => new Field
- {
- Count = string.IsNullOrEmpty(x.ElementCountSymbolic)
- ? x.ElementCount != 1 ? new Count(x.ElementCount) : null
- : new Count(x.ElementCountSymbolic, false),
- Name = Naming.Translate(TrimName(x.Name, task), prefix),
- Doc = $"/// {x.Comment}",
- NativeName = x.Name,
- NativeType = x.Type.ToString(),
- Type = ConvertType(x.Type),
- DefaultAssignment =
- (x.Type.Name == "VkStructureType" || x.Type.Name == "XrStructureType")
- && !string.IsNullOrWhiteSpace(x.LegalValues)
- ? "StructureType." + TryTrim
+ aList = new();
+ aliases[s.Alias] = aList;
+ }
+
+ aList.Add(s.Name);
+ continue;
+ }
+
+ var @struct = new Struct
+ {
+ Fields = s.Members.Select
+ (
+ x => new Field
+ {
+ Count = string.IsNullOrEmpty(x.ElementCountSymbolic)
+ ? x.ElementCount != 1 ? new Count(x.ElementCount) : null
+ : new Count(x.ElementCountSymbolic, false),
+ Name = Naming.Translate(TrimName(x.Name, task), prefix),
+ Doc = $"/// {x.Comment}",
+ NativeName = x.Name,
+ NativeType = x.Type.ToString(),
+ Type = ConvertType(x.Type),
+ DefaultAssignment =
+ (x.Type.Name == "VkStructureType" || x.Type.Name == "XrStructureType")
+ && !string.IsNullOrWhiteSpace(x.LegalValues)
+ ? "StructureType." + TryTrim
+ (
+ Naming.Translate
(
- Naming.Translate
- (
- TrimName(x.LegalValues.Split(',').FirstOrDefault(), task),
- task.FunctionPrefix
- ),
- Naming.TranslateLite(TrimName("VkStructureType", task), task.FunctionPrefix)
- )
- : null,
- NumBits = x.NumBits
- }.WithFixedFieldFixup09072020()
- )
- .ToList(),
- Name = Naming.TranslateLite(TrimName(s.Name, task), prefix),
- NativeName = s.Name
+ TrimName(x.LegalValues.Split(',').FirstOrDefault(), task),
+ task.FunctionPrefix
+ ),
+ Naming.TranslateLite(TrimName("VkStructureType", task), task.FunctionPrefix)
+ )
+ : null,
+ NumBits = x.NumBits
+ }.WithFixedFieldFixup09072020()
+ )
+ .ToList(),
+ Name = Naming.TranslateLite(TrimName(s.Name, task), prefix),
+ NativeName = s.Name
+ };
+
+ // Find the STYpe field (and it's position, which is required for IChainable
+ var (sTypeField, sTypeFieldIndex) = @struct.Fields.Select((f, i) => (Field: f, Index: i))
+ .FirstOrDefault(f => f.Field.Name == "SType" && f.Field.Type.Name == "VkStructureType");
+ if (sTypeField is not null)
+ {
+ @struct.Attributes.Add
+ (
+ new()
+ {
+ Name = "BuildToolsIntrinsic",
+ Arguments = new() {"$VKSTRUCTUREDTYPE", sTypeField.DefaultAssignment ?? string.Empty}
+ }
+ );
+
+ // Ensure SType was in position 0, and we have a pointer called PNext in position 1.
+ Field pNextField;
+ if (sTypeFieldIndex == 0 &&
+ @struct.Fields.Count > 1 &&
+ (pNextField = @struct.Fields[1]).Name == "PNext" &&
+ pNextField.Type.IsPointer)
+ {
+ // The type is at least chainable.
+ @struct.Attributes.Add
+ (
+ new()
+ {
+ Name = "BuildToolsIntrinsic",
+ Arguments = new() {"$VKCHAINABLE"}
+ }
+ );
+
+ if (s.Extends.Any())
+ {
+ chainExtensions.Add((@struct, s.Extends));
+ }
+ }
+ }
+
+ ret.Add(s.Name, @struct);
+ }
+
+ // Create Aliases
+ foreach (var (structName, aList) in aliases)
+ {
+ if (!ret.TryGetValue(structName, out var @struct))
+ {
+ continue;
+ }
+
+ foreach (var alias in aList)
+ {
+ var aliasStruct = @struct.Clone(Naming.TranslateLite(TrimName(alias, task), prefix), alias);
+ aliasStruct.Attributes.Add
+ (
+ new()
+ {
+ Name = "BuildToolsIntrinsic",
+ Arguments = new() {"$VKALIASOF", @struct.NativeName}
+ }
+ );
+ // Create a clone for the alias
+ ret.Add(alias, aliasStruct);
+ }
+
+ // Now that we've finished cloning we can add the build intrinsic to the root struct.
+ @struct.Attributes.Add
+ (
+ new()
+ {
+ Name = "BuildToolsIntrinsic",
+ Arguments = new[] {"$VKALIASES"}.Concat(aList).ToList()
}
);
}
-
+
+ // Add chain extensions, we have to do this now to account for aliases, we
+ if (chainExtensions.Any())
+ {
+ foreach (var (@struct, chainNames) in chainExtensions)
+ {
+ // Get all the aliases of this struct (including this one)
+ var allStructs = GetAllAliasesFromName(@struct.NativeName);
+ // Get all the chains this struct extends (including their aliases)
+ var chains = chainNames.SelectMany(n => GetAllAliasesFromName(n)).ToArray();
+
+ // Add $VKEXTENDSCHAIN build tools intrinsic attribute to all versions of this struct
+ Attribute attribute = null;
+ foreach (var s in allStructs)
+ {
+ if (attribute is null)
+ {
+ // Create $VKEXTENDSCHAIN build tools intrinsic attribute
+ attribute = new()
+ {
+ Name = "BuildToolsIntrinsic",
+ Arguments = new[] {"$VKEXTENDSCHAIN"}.Concat(chains.Select(c => c.Name)).ToList()
+ };
+ }
+ else
+ {
+ // Clone existing attribute.
+ attribute = attribute.Clone();
+ }
+
+ s.Attributes.Add(attribute);
+ }
+
+ // Add chain starts to all chains and their aliases
+ attribute = null;
+ foreach (var c in chains)
+ {
+ if (attribute is null)
+ {
+ // Create $VKEXTENDSCHAIN build tools intrinsic attribute
+ attribute = new()
+ {
+ Name = "BuildToolsIntrinsic",
+ Arguments = new[] {"$VKCHAINSTART"}.Concat(allStructs.Select(s => s.Name)).ToList()
+ };
+ }
+ else
+ {
+ // Clone existing attribute.
+ attribute = attribute.Clone();
+ }
+
+ c.Attributes.Add(attribute);
+ }
+ }
+ }
+
foreach (var h in spec.Handles)
{
ret.Add
@@ -103,7 +302,9 @@ private Dictionary ConvertStructs(VulkanSpecification spec, Bind
h.Name, new Struct
{
Fields = new List
- {new Field {Name = "Handle", Type = new Type {Name = h.CanBeDispatched ? "nint" : "ulong"}}},
+ {
+ new Field {Name = "Handle", Type = new Type {Name = h.CanBeDispatched ? "nint" : "ulong"}}
+ },
Name = Naming.TranslateLite(TrimName(h.Name, task), prefix),
NativeName = h.Name
}
@@ -112,13 +313,19 @@ private Dictionary ConvertStructs(VulkanSpecification spec, Bind
foreach (var u in spec.Unions)
{
- ret.Add(u.Name, new Struct
- {
- Attributes = new List{new Attribute{Name = "StructLayout", Arguments = new List{"LayoutKind.Explicit"}}},
- Fields = GetFields(u, task).ToList(),
- Name = Naming.TranslateLite(TrimName(u.Name, task), prefix),
- NativeName = u.Name
- });
+ ret.Add
+ (
+ u.Name, new Struct
+ {
+ Attributes = new List
+ {
+ new Attribute {Name = "StructLayout", Arguments = new List {"LayoutKind.Explicit"}}
+ },
+ Fields = GetFields(u, task).ToList(),
+ Name = Naming.TranslateLite(TrimName(u.Name, task), prefix),
+ NativeName = u.Name
+ }
+ );
}
return ret;
@@ -138,7 +345,7 @@ private IEnumerable GetFields(StructureDefinition union, BindTask task)
Name = $"{Naming.Translate(x.Name, task.FunctionPrefix)}_{i}",
Attributes = new List
{
- new Attribute{Name = "FieldOffset", Arguments = new List {$"{i * fieldSize}"}}
+ new Attribute {Name = "FieldOffset", Arguments = new List {$"{i * fieldSize}"}}
},
Doc = $"/// {x.Comment}",
NativeName = x.Name,
@@ -155,7 +362,7 @@ private IEnumerable GetFields(StructureDefinition union, BindTask task)
Name = $"{Naming.Translate(x.Name, task.FunctionPrefix)}",
Attributes = new List
{
- new Attribute{Name = "FieldOffset", Arguments = new List {"0"}}
+ new Attribute {Name = "FieldOffset", Arguments = new List {"0"}}
},
Doc = $"/// {x.Comment}",
NativeName = x.Name,
@@ -165,7 +372,7 @@ private IEnumerable GetFields(StructureDefinition union, BindTask task)
};
}
}
- }
+ }
private int GetTypeSize(string type, IEnumerable> maps)
{
@@ -237,7 +444,7 @@ public IEnumerable ReadFunctions(object obj, BindTask task)
}
}
}
-
+
foreach (var extension in spec.Extensions)
{
foreach (var name in extension.CommandNames)
@@ -273,14 +480,15 @@ private Dictionary ConvertFunctions(VulkanSpecification spec,
(
function.Name, new Function
{
- Name = Naming.Translate(NameTrimmer.Trim(TrimName(function.Name, task), task.FunctionPrefix), task.FunctionPrefix),
+ Name = Naming.Translate
+ (NameTrimmer.Trim(TrimName(function.Name, task), task.FunctionPrefix), task.FunctionPrefix),
Parameters = function.Parameters.Select
(
x => new Parameter
{
Count = x.IsNullTerminated ? null :
- x.ElementCountSymbolic != null ?
- function.Parameters.Any(y => y.Name == x.ElementCountSymbolic)
+ x.ElementCountSymbolic != null ? function.Parameters.Any
+ (y => y.Name == x.ElementCountSymbolic)
? new(x.ElementCountSymbolic)
: new(x.ElementCountSymbolic.Split(',')) :
new(x.ElementCount),
@@ -322,7 +530,7 @@ public IEnumerable ReadEnums(object obj, BindTask task)
};
}
}
-
+
task.InjectTypeMap(tm);
}
@@ -342,7 +550,7 @@ public IEnumerable ReadConstants(object obj, BindTask task)
ConstantType.Float32 => new Type {Name = "float"},
ConstantType.UInt32 => new Type {Name = "uint"},
ConstantType.UInt64 => new Type {Name = "ulong"},
- _ => new Type{Name = "ulong"}
+ _ => new Type {Name = "ulong"}
},
ExtensionName = "Core"
}
@@ -363,21 +571,23 @@ public IEnumerable ReadConstants(object obj, BindTask task)
}
)
)
- ).Concat
+ )
+ .Concat
(
spec.Extensions.SelectMany
(
- x => x.EnumExtensions.Where(y => y.ExtendedType is null).Select
- (
- y => new Constant
- {
- Name = Naming.Translate(TrimName(y.Name, task), task.FunctionPrefix),
- NativeName = y.Name,
- Value = y.Value,
- Type = new Type {Name = "uint"},
- ExtensionName = TrimName(x.Name, task)
- }
- )
+ x => x.EnumExtensions.Where(y => y.ExtendedType is null)
+ .Select
+ (
+ y => new Constant
+ {
+ Name = Naming.Translate(TrimName(y.Name, task), task.FunctionPrefix),
+ NativeName = y.Name,
+ Value = y.Value,
+ Type = new Type {Name = "uint"},
+ ExtensionName = TrimName(x.Name, task)
+ }
+ )
)
);
}
@@ -395,7 +605,9 @@ public string TrimName(string name, BindTask task)
return name.Remove(0, task.FunctionPrefix.Length + 1);
}
- return name.ToLower().StartsWith(task.FunctionPrefix.ToLower()) ? name.Remove(0, task.FunctionPrefix.Length) : name;
+ return name.ToLower().StartsWith(task.FunctionPrefix.ToLower())
+ ? name.Remove(0, task.FunctionPrefix.Length)
+ : name;
}
private static FlowDirection ConvertFlow(ParameterModifier mod)
@@ -408,7 +620,7 @@ private static FlowDirection ConvertFlow(ParameterModifier mod)
_ => FlowDirection.In
};
}
-
+
private Dictionary ConvertEnums(VulkanSpecification spec, BindTask task)
{
var ret = new Dictionary();
@@ -440,8 +652,8 @@ private Dictionary ConvertEnums(VulkanSpecification spec, BindTask
: new List(),
EnumBaseType = e.BitWidth switch
{
- 64 => new(){Name = "long"},
- _ => new(){Name = "int"}
+ 64 => new() {Name = "long"},
+ _ => new() {Name = "int"}
}
}
);
@@ -458,6 +670,7 @@ private Dictionary ConvertEnums(VulkanSpecification spec, BindTask
}
private static readonly char[] Digits = "1234567890".ToCharArray();
+
private static string TryTrim(string token, string @enum)
{
var trimmed = token.StartsWith(@enum) ? token.Substring(@enum.Length) : token;
diff --git a/src/Lab/Experiments/PrototypeStructChaining/.gitignore b/src/Lab/Experiments/PrototypeStructChaining/.gitignore
new file mode 100644
index 0000000000..a83c78113b
--- /dev/null
+++ b/src/Lab/Experiments/PrototypeStructChaining/.gitignore
@@ -0,0 +1,341 @@
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+##
+## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
+
+# User-specific files
+*.rsuser
+*.suo
+*.user
+*.userosscache
+*.sln.docstates
+
+# User-specific files (MonoDevelop/Xamarin Studio)
+*.userprefs
+
+# Build results
+[Dd]ebug/
+[Dd]ebugPublic/
+[Rr]elease/
+[Rr]eleases/
+x64/
+x86/
+[Aa][Rr][Mm]/
+[Aa][Rr][Mm]64/
+bld/
+[Bb]in/
+[Oo]bj/
+[Ll]og/
+
+# Visual Studio 2015/2017 cache/options directory
+.vs/
+# Uncomment if you have tasks that create the project's static files in wwwroot
+#wwwroot/
+
+# Visual Studio 2017 auto generated files
+Generated\ Files/
+
+# MSTest test Results
+[Tt]est[Rr]esult*/
+[Bb]uild[Ll]og.*
+
+# NUNIT
+*.VisualState.xml
+TestResult.xml
+
+# Build Results of an ATL Project
+[Dd]ebugPS/
+[Rr]eleasePS/
+dlldata.c
+
+# Benchmark Results
+BenchmarkDotNet.Artifacts/
+
+# .NET Core
+project.lock.json
+project.fragment.lock.json
+artifacts/
+
+# StyleCop
+StyleCopReport.xml
+
+# Files built by Visual Studio
+*_i.c
+*_p.c
+*_h.h
+*.ilk
+*.meta
+*.obj
+*.iobj
+*.pch
+*.pdb
+*.ipdb
+*.pgc
+*.pgd
+*.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.tmp_proj
+*_wpftmp.csproj
+*.log
+*.vspscc
+*.vssscc
+.builds
+*.pidb
+*.svclog
+*.scc
+
+# Chutzpah Test files
+_Chutzpah*
+
+# Visual C++ cache files
+ipch/
+*.aps
+*.ncb
+*.opendb
+*.opensdf
+*.sdf
+*.cachefile
+*.VC.db
+*.VC.VC.opendb
+
+# Visual Studio profiler
+*.psess
+*.vsp
+*.vspx
+*.sap
+
+# Visual Studio Trace Files
+*.e2e
+
+# TFS 2012 Local Workspace
+$tf/
+
+# Guidance Automation Toolkit
+*.gpState
+
+# ReSharper is a .NET coding add-in
+_ReSharper*/
+*.[Rr]e[Ss]harper
+*.DotSettings.user
+
+# JustCode is a .NET coding add-in
+.JustCode
+
+# TeamCity is a build add-in
+_TeamCity*
+
+# DotCover is a Code Coverage Tool
+*.dotCover
+
+# AxoCover is a Code Coverage Tool
+.axoCover/*
+!.axoCover/settings.json
+
+# Visual Studio code coverage results
+*.coverage
+*.coveragexml
+
+# NCrunch
+_NCrunch_*
+.*crunch*.local.xml
+nCrunchTemp_*
+
+# MightyMoose
+*.mm.*
+AutoTest.Net/
+
+# Web workbench (sass)
+.sass-cache/
+
+# Installshield output folder
+[Ee]xpress/
+
+# DocProject is a documentation generator add-in
+DocProject/buildhelp/
+DocProject/Help/*.HxT
+DocProject/Help/*.HxC
+DocProject/Help/*.hhc
+DocProject/Help/*.hhk
+DocProject/Help/*.hhp
+DocProject/Help/Html2
+DocProject/Help/html
+
+# Click-Once directory
+publish/
+
+# Publish Web Output
+*.[Pp]ublish.xml
+*.azurePubxml
+# Note: Comment the next line if you want to checkin your web deploy settings,
+# but database connection strings (with potential passwords) will be unencrypted
+*.pubxml
+*.publishproj
+
+# Microsoft Azure Web App publish settings. Comment the next line if you want to
+# checkin your Azure Web App publish settings, but sensitive information contained
+# in these scripts will be unencrypted
+PublishScripts/
+
+# NuGet Packages
+*.nupkg
+# The packages folder can be ignored because of Package Restore
+**/[Pp]ackages/*
+# except build/, which is used as an MSBuild target.
+!**/[Pp]ackages/build/
+# Uncomment if necessary however generally it will be regenerated when needed
+#!**/[Pp]ackages/repositories.config
+# NuGet v3's project.json files produces more ignorable files
+*.nuget.props
+*.nuget.targets
+
+# Microsoft Azure Build Output
+csx/
+*.build.csdef
+
+# Microsoft Azure Emulator
+ecf/
+rcf/
+
+# Windows Store app package directories and files
+AppPackages/
+BundleArtifacts/
+Package.StoreAssociation.xml
+_pkginfo.txt
+*.appx
+
+# Visual Studio cache files
+# files ending in .cache can be ignored
+*.[Cc]ache
+# but keep track of directories ending in .cache
+!?*.[Cc]ache/
+
+# Others
+ClientBin/
+~$*
+*~
+*.dbmdl
+*.dbproj.schemaview
+*.jfm
+*.pfx
+*.publishsettings
+orleans.codegen.cs
+
+# Including strong name files can present a security risk
+# (https://github.com/github/gitignore/pull/2483#issue-259490424)
+#*.snk
+
+# Since there are multiple workflows, uncomment next line to ignore bower_components
+# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
+#bower_components/
+
+# RIA/Silverlight projects
+Generated_Code/
+
+# Backup & report files from converting an old project file
+# to a newer Visual Studio version. Backup files are not needed,
+# because we have git ;-)
+_UpgradeReport_Files/
+Backup*/
+UpgradeLog*.XML
+UpgradeLog*.htm
+ServiceFabricBackup/
+*.rptproj.bak
+
+# SQL Server files
+*.mdf
+*.ldf
+*.ndf
+
+# Business Intelligence projects
+*.rdl.data
+*.bim.layout
+*.bim_*.settings
+*.rptproj.rsuser
+*- Backup*.rdl
+
+# Microsoft Fakes
+FakesAssemblies/
+
+# GhostDoc plugin setting file
+*.GhostDoc.xml
+
+# Node.js Tools for Visual Studio
+.ntvs_analysis.dat
+node_modules/
+
+# Visual Studio 6 build log
+*.plg
+
+# Visual Studio 6 workspace options file
+*.opt
+
+# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
+*.vbw
+
+# Visual Studio LightSwitch build output
+**/*.HTMLClient/GeneratedArtifacts
+**/*.DesktopClient/GeneratedArtifacts
+**/*.DesktopClient/ModelManifest.xml
+**/*.Server/GeneratedArtifacts
+**/*.Server/ModelManifest.xml
+_Pvt_Extensions
+
+# Paket dependency manager
+.paket/paket.exe
+paket-files/
+
+# FAKE - F# Make
+.fake/
+
+# JetBrains Rider
+.idea/**/workspace.xml
+.idea/**/usage.statistics.xml
+.idea/**/shelf/
+
+# CodeRush personal settings
+.cr/personal
+
+# Python Tools for Visual Studio (PTVS)
+__pycache__/
+*.pyc
+
+# Cake - Uncomment if you are using it
+# tools/**
+# !tools/packages.config
+
+# Tabs Studio
+*.tss
+
+# Telerik's JustMock configuration file
+*.jmconfig
+
+# BizTalk build output
+*.btp.cs
+*.btm.cs
+*.odx.cs
+*.xsd.cs
+
+# OpenCover UI analysis results
+OpenCover/
+
+# Azure Stream Analytics local run output
+ASALocalRun/
+
+# MSBuild Binary and Structured Log
+*.binlog
+
+# NVidia Nsight GPU debugger configuration file
+*.nvuser
+
+# MFractors (Xamarin productivity tool) working folder
+.mfractor/
+
+# Local History for Visual Studio
+.localhistory/
+
+# BeatPulse healthcheck temp database
+healthchecksdb
\ No newline at end of file
diff --git a/src/Lab/Experiments/PrototypeStructChaining/.idea/.idea.PrototypeStructChaining/.idea/.gitignore b/src/Lab/Experiments/PrototypeStructChaining/.idea/.idea.PrototypeStructChaining/.idea/.gitignore
new file mode 100644
index 0000000000..009550ae99
--- /dev/null
+++ b/src/Lab/Experiments/PrototypeStructChaining/.idea/.idea.PrototypeStructChaining/.idea/.gitignore
@@ -0,0 +1,13 @@
+# Default ignored files
+/shelf/
+/workspace.xml
+# Rider ignored files
+/modules.xml
+/contentModel.xml
+/.idea.PrototypeStructChaining.iml
+/projectSettingsUpdater.xml
+# Editor-based HTTP Client requests
+/httpRequests/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml
diff --git a/src/Lab/Experiments/PrototypeStructChaining/.idea/.idea.PrototypeStructChaining/.idea/encodings.xml b/src/Lab/Experiments/PrototypeStructChaining/.idea/.idea.PrototypeStructChaining/.idea/encodings.xml
new file mode 100644
index 0000000000..df87cf951f
--- /dev/null
+++ b/src/Lab/Experiments/PrototypeStructChaining/.idea/.idea.PrototypeStructChaining/.idea/encodings.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/src/Lab/Experiments/PrototypeStructChaining/.idea/.idea.PrototypeStructChaining/.idea/indexLayout.xml b/src/Lab/Experiments/PrototypeStructChaining/.idea/.idea.PrototypeStructChaining/.idea/indexLayout.xml
new file mode 100644
index 0000000000..7b08163ceb
--- /dev/null
+++ b/src/Lab/Experiments/PrototypeStructChaining/.idea/.idea.PrototypeStructChaining/.idea/indexLayout.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Lab/Experiments/PrototypeStructChaining/.idea/.idea.PrototypeStructChaining/.idea/vcs.xml b/src/Lab/Experiments/PrototypeStructChaining/.idea/.idea.PrototypeStructChaining/.idea/vcs.xml
new file mode 100644
index 0000000000..029a1a823f
--- /dev/null
+++ b/src/Lab/Experiments/PrototypeStructChaining/.idea/.idea.PrototypeStructChaining/.idea/vcs.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Lab/Experiments/PrototypeStructChaining/PrototypeStructChaining.Test/PrototypeStructChaining.Test.csproj b/src/Lab/Experiments/PrototypeStructChaining/PrototypeStructChaining.Test/PrototypeStructChaining.Test.csproj
new file mode 100644
index 0000000000..c61c1205b8
--- /dev/null
+++ b/src/Lab/Experiments/PrototypeStructChaining/PrototypeStructChaining.Test/PrototypeStructChaining.Test.csproj
@@ -0,0 +1,34 @@
+
+
+
+ net6.0
+ enable
+
+ false
+
+ true
+
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Lab/Experiments/PrototypeStructChaining/PrototypeStructChaining.Test/TestChainMetadata.cs b/src/Lab/Experiments/PrototypeStructChaining/PrototypeStructChaining.Test/TestChainMetadata.cs
new file mode 100644
index 0000000000..9e3c26a1f4
--- /dev/null
+++ b/src/Lab/Experiments/PrototypeStructChaining/PrototypeStructChaining.Test/TestChainMetadata.cs
@@ -0,0 +1,58 @@
+using System;
+using System.Linq;
+using Silk.NET.Vulkan;
+using Xunit;
+
+namespace PrototypeStructChaining.Test;
+
+public class TestChainMetadata
+{
+ [Fact]
+ public void TestClrType()
+ {
+ Assert.Equal(typeof(DeviceCreateInfo), StructureType.DeviceCreateInfo.ClrType());
+ }
+
+ [Fact]
+ public void TestStructureType()
+ {
+ Assert.Equal(StructureType.DeviceCreateInfo, typeof(DeviceCreateInfo).StructureType());
+ Assert.Null(typeof(PhysicalDeviceFeatures).StructureType());
+ }
+
+ [Fact]
+ public void TestIsChainStart()
+ {
+ Assert.True(StructureType.DeviceCreateInfo.IsChainStart());
+ Assert.True(typeof(DeviceCreateInfo).IsChainStart());
+ Assert.False(StructureType.PhysicalDeviceDescriptorIndexingFeatures.IsChainStart());
+ Assert.False(typeof(PhysicalDeviceDescriptorIndexingFeatures).IsChainStart());
+ }
+
+ [Fact]
+ public void TestIsChainable()
+ {
+ Assert.True(StructureType.DeviceCreateInfo.IsChainable());
+ Assert.True(typeof(DeviceCreateInfo).IsChainable());
+ Assert.True(StructureType.PhysicalDeviceDescriptorIndexingFeatures.IsChainable());
+ Assert.True(typeof(PhysicalDeviceDescriptorIndexingFeatures).IsChainable());
+ }
+
+ [Fact]
+ public void TestCanExtend()
+ {
+ Assert.True(StructureType.PhysicalDeviceFeatures2.CanExtend(StructureType.DeviceCreateInfo));
+ Assert.False(StructureType.DeviceCreateInfo.CanExtend(StructureType.PhysicalDeviceFeatures2));
+ Assert.True(typeof(PhysicalDeviceFeatures2).CanExtend(typeof(DeviceCreateInfo)));
+ Assert.False(typeof(DeviceCreateInfo).CanExtend(typeof(PhysicalDeviceFeatures2)));
+ }
+
+ [Fact]
+ public void TestCanBeExtendedBy()
+ {
+ Assert.False(StructureType.PhysicalDeviceFeatures2.CanBeExtendedBy(StructureType.DeviceCreateInfo));
+ Assert.True(StructureType.DeviceCreateInfo.CanBeExtendedBy(StructureType.PhysicalDeviceFeatures2));
+ Assert.False(typeof(PhysicalDeviceFeatures2).CanBeExtendedBy(typeof(DeviceCreateInfo)));
+ Assert.True(typeof(DeviceCreateInfo).CanBeExtendedBy(typeof(PhysicalDeviceFeatures2)));
+ }
+}
diff --git a/src/Lab/Experiments/PrototypeStructChaining/PrototypeStructChaining.Test/TestChains.cs b/src/Lab/Experiments/PrototypeStructChaining/PrototypeStructChaining.Test/TestChains.cs
new file mode 100644
index 0000000000..40f1d40ce3
--- /dev/null
+++ b/src/Lab/Experiments/PrototypeStructChaining/PrototypeStructChaining.Test/TestChains.cs
@@ -0,0 +1,183 @@
+using Silk.NET.Vulkan;
+using Xunit;
+
+namespace PrototypeStructChaining.Test;
+
+public class TestChains
+{
+ [Fact]
+ public unsafe void TestAddNext()
+ {
+ PhysicalDeviceFeatures2
+ // The BaseInStructure method, is a convenient static, to provide a consistent syntax.
+ .Chain(out var features2)
+ // AddNext will create an empty struct, with the correct SType (as well as ensuring the
+ // chain's SType is coerced correctly.
+ .AddNext(out PhysicalDeviceDescriptorIndexingFeatures indexingFeatures)
+ .AddNext(out PhysicalDeviceAccelerationStructureFeaturesKHR accelerationStructureFeaturesKhr);
+
+ // Ensure all pointers set correctly
+ Assert.Equal((nint) (&indexingFeatures), (nint) features2.PNext);
+ Assert.Equal((nint) (&accelerationStructureFeaturesKhr), (nint) indexingFeatures.PNext);
+ Assert.Equal(0, (nint) accelerationStructureFeaturesKhr.PNext);
+
+ // Ensure all STypes set correctly
+ Assert.Equal(StructureType.PhysicalDeviceFeatures2, features2.SType);
+ Assert.Equal(StructureType.PhysicalDeviceDescriptorIndexingFeatures, indexingFeatures.SType);
+ Assert.Equal
+ (
+ StructureType.PhysicalDeviceAccelerationStructureFeaturesKhr,
+ accelerationStructureFeaturesKhr.SType
+ );
+
+ // Check indices
+ Assert.Equal(1, features2.IndexOf(ref indexingFeatures));
+ Assert.Equal(2, features2.IndexOf(ref accelerationStructureFeaturesKhr));
+ }
+
+ [Fact]
+ public unsafe void TestTryAddNext()
+ {
+ PhysicalDeviceFeatures2
+ // The BaseInStructure method, is a convenient static, to provide a consistent syntax.
+ .Chain(out var features2)
+ // AddNext will create an empty struct, with the correct SType (as well as ensuring the
+ // chain's SType is coerced correctly.
+ .AddNext(out PhysicalDeviceDescriptorIndexingFeatures indexingFeatures);
+
+ // Ensure all pointers set correctly
+ Assert.Equal((nint) (&indexingFeatures), (nint) features2.PNext);
+
+ // Ensure all STypes set correctly
+ Assert.Equal(StructureType.PhysicalDeviceFeatures2, features2.SType);
+ Assert.Equal(StructureType.PhysicalDeviceDescriptorIndexingFeatures, indexingFeatures.SType);
+
+ features2.TryAddNext(out PhysicalDeviceDescriptorIndexingFeatures indexingFeatures2, out var added);
+ Assert.False(added);
+ }
+
+ [Fact]
+ public unsafe void TestSetNext()
+ {
+ var indexingFeatures = new PhysicalDeviceDescriptorIndexingFeatures
+ {
+ ShaderInputAttachmentArrayDynamicIndexing = true
+ };
+ var accelerationStructureFeaturesKhr = new PhysicalDeviceAccelerationStructureFeaturesKHR
+ {
+ AccelerationStructure = true
+ };
+
+ 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 accelerationStructureFeaturesKhr);
+
+ Assert.Equal((nint) (&indexingFeatures), (nint) features2.PNext);
+ Assert.Equal((nint) (&accelerationStructureFeaturesKhr), (nint) indexingFeatures.PNext);
+ Assert.Equal(0, (nint) accelerationStructureFeaturesKhr.PNext);
+
+ Assert.Equal(StructureType.PhysicalDeviceFeatures2, features2.SType);
+ Assert.Equal(StructureType.PhysicalDeviceDescriptorIndexingFeatures, indexingFeatures.SType);
+ Assert.Equal
+ (
+ StructureType.PhysicalDeviceAccelerationStructureFeaturesKhr,
+ accelerationStructureFeaturesKhr.SType
+ );
+
+ Assert.True(indexingFeatures.ShaderInputAttachmentArrayDynamicIndexing);
+ Assert.True(accelerationStructureFeaturesKhr.AccelerationStructure);
+ }
+
+ [Fact]
+ public unsafe void TestSetNextUpdates()
+ {
+ var indexingFeatures = new PhysicalDeviceDescriptorIndexingFeatures
+ {
+ ShaderInputAttachmentArrayDynamicIndexing = true
+ };
+
+ PhysicalDeviceFeatures2
+ .Chain(out var features2)
+ // SetNext accepts an existing struct, note, it will coerce the SType and blank the PNext
+ .SetNext(ref indexingFeatures);
+
+ Assert.Equal((nint) (&indexingFeatures), (nint) features2.PNext);
+ Assert.Equal(0, (nint) indexingFeatures.PNext);
+
+ Assert.Equal(StructureType.PhysicalDeviceFeatures2, features2.SType);
+ Assert.Equal(StructureType.PhysicalDeviceDescriptorIndexingFeatures, indexingFeatures.SType);
+
+ // Update indexing features
+ var indexingFeatures2 = new PhysicalDeviceDescriptorIndexingFeatures
+ {
+ ShaderInputAttachmentArrayDynamicIndexing = false
+ };
+ features2.SetNext(ref indexingFeatures2);
+
+ Assert.Equal((nint) (&indexingFeatures2), (nint) features2.PNext);
+ Assert.Equal(0, (nint) indexingFeatures2.PNext);
+
+ Assert.Equal(StructureType.PhysicalDeviceDescriptorIndexingFeatures, indexingFeatures2.SType);
+
+ Assert.Equal(1, features2.IndexOf(ref indexingFeatures2));
+ Assert.True(features2.IndexOf(ref indexingFeatures) < 0);
+ }
+
+ [Fact]
+ public unsafe void TestSetNextAlwaysAdd()
+ {
+ var indexingFeatures = new PhysicalDeviceDescriptorIndexingFeatures
+ {
+ ShaderInputAttachmentArrayDynamicIndexing = true
+ };
+
+ PhysicalDeviceFeatures2
+ .Chain(out var features2)
+ // SetNext accepts an existing struct, note, it will coerce the SType and blank the PNext
+ .SetNext(ref indexingFeatures);
+
+ Assert.Equal((nint) (&indexingFeatures), (nint) features2.PNext);
+ Assert.Equal(0, (nint) indexingFeatures.PNext);
+
+ Assert.Equal(StructureType.PhysicalDeviceFeatures2, features2.SType);
+ Assert.Equal(StructureType.PhysicalDeviceDescriptorIndexingFeatures, indexingFeatures.SType);
+
+ // Update indexing features
+ var indexingFeatures2 = new PhysicalDeviceDescriptorIndexingFeatures
+ {
+ ShaderInputAttachmentArrayDynamicIndexing = false
+ };
+ features2.SetNext(ref indexingFeatures2, true);
+
+ Assert.Equal((nint) (&indexingFeatures), (nint) features2.PNext);
+ Assert.Equal((nint) (&indexingFeatures2), (nint) indexingFeatures.PNext);
+ Assert.Equal(0, (nint) indexingFeatures2.PNext);
+
+ Assert.Equal(StructureType.PhysicalDeviceDescriptorIndexingFeatures, indexingFeatures2.SType);
+
+ Assert.Equal(1, features2.IndexOf(ref indexingFeatures));
+ Assert.Equal(2, features2.IndexOf(ref indexingFeatures2));
+ }
+
+ [Fact]
+ public unsafe void TestWithoutChain()
+ {
+ // We don't have to use the BaseInStructure() pattern, as we can start with an existing struct
+ var createInfo = new DeviceCreateInfo
+ {
+ Flags = 1U
+ };
+ // However, note that AddNext will still coerce the SType of createInfo.
+ createInfo.AddNext(out PhysicalDeviceFeatures2 features2);
+ Assert.Equal((nint) (&features2), (nint) createInfo.PNext);
+ Assert.Equal(0, (nint) features2.PNext);
+
+ // Note, even though we didn't use chain, we have still coerced the SType
+ Assert.Equal(StructureType.DeviceCreateInfo, createInfo.SType);
+ Assert.Equal(StructureType.PhysicalDeviceFeatures2, features2.SType);
+
+ Assert.Equal(1U, createInfo.Flags);
+ }
+}
diff --git a/src/Lab/Experiments/PrototypeStructChaining/PrototypeStructChaining.Test/TestChainsAny.cs b/src/Lab/Experiments/PrototypeStructChaining/PrototypeStructChaining.Test/TestChainsAny.cs
new file mode 100644
index 0000000000..f7e440dab8
--- /dev/null
+++ b/src/Lab/Experiments/PrototypeStructChaining/PrototypeStructChaining.Test/TestChainsAny.cs
@@ -0,0 +1,30 @@
+using Silk.NET.Vulkan;
+using Xunit;
+
+namespace PrototypeStructChaining.Test;
+
+public class TestChainsAny
+{
+ [Fact]
+ public unsafe void TestAddNextUnchecked()
+ {
+ var accelerationStructureFeatures = new PhysicalDeviceAccelerationStructureFeaturesKHR();
+ accelerationStructureFeatures
+ .AddNextAny(out PhysicalDeviceDescriptorIndexingFeatures indexingFeatures)
+ .AddNextAny(out DeviceCreateInfo deviceCreateInfo);
+
+ // Ensure all pointers set correctly
+ Assert.Equal((nint) (&indexingFeatures), (nint) accelerationStructureFeatures.PNext);
+ Assert.Equal((nint) (&deviceCreateInfo), (nint) indexingFeatures.PNext);
+ Assert.Equal(0, (nint) deviceCreateInfo.PNext);
+
+ // Ensure all STypes set correctly
+ Assert.Equal(StructureType.PhysicalDeviceAccelerationStructureFeaturesKhr, accelerationStructureFeatures.SType);
+ Assert.Equal(StructureType.PhysicalDeviceDescriptorIndexingFeatures, indexingFeatures.SType);
+ Assert.Equal(StructureType.DeviceCreateInfo, deviceCreateInfo.SType);
+
+ // Check indices
+ Assert.Equal(1, accelerationStructureFeatures.IndexOfAny(ref indexingFeatures));
+ Assert.Equal(2, accelerationStructureFeatures.IndexOfAny(ref deviceCreateInfo));
+ }
+}
diff --git a/src/Lab/Experiments/PrototypeStructChaining/PrototypeStructChaining.Test/TestCompilation.cs b/src/Lab/Experiments/PrototypeStructChaining/PrototypeStructChaining.Test/TestCompilation.cs
new file mode 100644
index 0000000000..5f27487a37
--- /dev/null
+++ b/src/Lab/Experiments/PrototypeStructChaining/PrototypeStructChaining.Test/TestCompilation.cs
@@ -0,0 +1,107 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.Emit;
+using Silk.NET.Vulkan;
+using Xunit;
+
+namespace PrototypeStructChaining.Test;
+
+public class TestCompilation
+{
+ private static readonly Lazy> References = new
+ (
+ () =>
+ ((string?) AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES") ?? string.Empty)
+ .Split(Path.PathSeparator)
+ .Select(r => MetadataReference.CreateFromFile(r))
+ .Concat(new[] {MetadataReference.CreateFromFile(typeof(StructureType).Assembly.Location)})
+ .ToArray()
+ );
+
+ private static readonly string CodeTemplate = @"
+using System;
+using Silk.NET.Vulkan;
+
+public class Test
+{{
+ public void DoTest()
+ {{
+ {0}
+ }}
+}}";
+
+ private IReadOnlyList CheckCompile(string code)
+ {
+ var assemblyName = Path.GetRandomFileName();
+ var compilation = CSharpCompilation.Create
+ (
+ assemblyName,
+ new[] {CSharpSyntaxTree.ParseText(string.Format(CodeTemplate, code))},
+ References.Value,
+ new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)
+ );
+
+ using var ms = new MemoryStream();
+ var result = compilation.Emit(ms);
+
+ if (result.Success)
+ {
+ return Array.Empty();
+ }
+
+ return result.Diagnostics.Where
+ (
+ diagnostic =>
+ diagnostic.IsWarningAsError ||
+ diagnostic.Severity == DiagnosticSeverity.Error
+ )
+ .ToArray();
+ }
+
+ [Fact]
+ public void TestCantAddUnsupportedNext()
+ {
+ var diagnostics = CheckCompile
+ (
+ @"PhysicalDeviceFeatures2
+ .Chain(out var features2)
+ .AddNext(out DeviceCreateInfo createInfo);"
+ );
+
+ Assert.Single(diagnostics);
+ var error = diagnostics.First();
+ // error CS0315: The type 'Silk.NET.Vulkan.PhysicalDeviceFeatures2' cannot be used as type parameter 'TChain' in the generic type or method 'Chain.AddNext(ref TChain, out TNext)'. There is no boxing conversion from 'Silk.NET.Vulkan.PhysicalDeviceFeatures2' to 'Silk.NET.Vulkan.IChainable'.
+ Assert.Equal("CS0315", error.Id);
+ }
+
+ [Fact]
+ public void TestCanAddUnsupportedNextUsingAny()
+ {
+ var diagnostics = CheckCompile
+ (
+ @"PhysicalDeviceFeatures2
+ .Chain(out var features2)
+ .AddNextAny(out DeviceCreateInfo createInfo);"
+ );
+
+ Assert.Empty(diagnostics);
+ }
+
+ [Fact]
+ public void TestCanAddSupportedNext()
+ {
+ var diagnostics = CheckCompile
+ (
+ @"DeviceCreateInfo
+ .Chain(out var createInfo)
+ .AddNext(out PhysicalDeviceFeatures2 features2);"
+ );
+
+ Assert.Empty(diagnostics);
+ }
+}
diff --git a/src/Lab/Experiments/PrototypeStructChaining/PrototypeStructChaining.Test/TestManagedChains.cs b/src/Lab/Experiments/PrototypeStructChaining/PrototypeStructChaining.Test/TestManagedChains.cs
new file mode 100644
index 0000000000..df8663701a
--- /dev/null
+++ b/src/Lab/Experiments/PrototypeStructChaining/PrototypeStructChaining.Test/TestManagedChains.cs
@@ -0,0 +1,453 @@
+using System;
+using System.Linq;
+using Silk.NET.Vulkan;
+using Xunit;
+
+namespace PrototypeStructChaining.Test;
+
+public class TestManagedChains
+{
+ [Fact]
+ public unsafe void TestManagedChain()
+ {
+ using var chain = ManagedChain.Create
+ (
+ default(PhysicalDeviceFeatures2),
+ default(PhysicalDeviceDescriptorIndexingFeatures),
+ default(PhysicalDeviceAccelerationStructureFeaturesKHR)
+ );
+
+ // Ensure all STypes set correctly
+ Assert.Equal(StructureType.PhysicalDeviceFeatures2, chain.Head.SType);
+ Assert.Equal(StructureType.PhysicalDeviceDescriptorIndexingFeatures, chain.Item1.SType);
+ Assert.Equal(StructureType.PhysicalDeviceAccelerationStructureFeaturesKhr, chain.Item2.SType);
+
+ // Ensure pointers set correctly
+ Assert.Equal((nint) chain.Item1Ptr, (nint) chain.Head.PNext);
+ Assert.Equal((nint) chain.Item2Ptr, (nint) chain.Item1.PNext);
+ Assert.Equal(0, (nint) chain.Item2.PNext);
+ }
+
+ [Fact]
+ public unsafe void TestManagedChainReplaceHead()
+ {
+ using var chain =
+ ManagedChain.Create
+ (
+ default(DeviceCreateInfo),
+ default(PhysicalDeviceFeatures2),
+ default(PhysicalDeviceDescriptorIndexingFeatures)
+ );
+
+ // Ensure all STypes set correctly
+ Assert.Equal(StructureType.DeviceCreateInfo, chain.Head.SType);
+ Assert.Equal(StructureType.PhysicalDeviceFeatures2, chain.Item1.SType);
+ Assert.Equal(StructureType.PhysicalDeviceDescriptorIndexingFeatures, chain.Item2.SType);
+
+ // Ensure pointers set correctly
+ Assert.Equal((nint) chain.Item1Ptr, (nint) chain.Head.PNext);
+ Assert.Equal((nint) chain.Item2Ptr, (nint) chain.Item1.PNext);
+ Assert.Equal(0, (nint) chain.Item2.PNext);
+
+ Assert.Equal(0U, chain.Head.Flags);
+
+ var headPtr = chain.HeadPtr;
+
+ // Get the current head
+ var head = chain.Head;
+ // Update the flags
+ head.Flags = 1U;
+ // Update the chain
+ chain.Head = head;
+
+ Assert.Equal(1U, chain.Head.Flags);
+
+ // The head ptr should not change, we overwrite it with the new value
+ Assert.Equal((nint) headPtr, (nint) chain.HeadPtr);
+ // But the next pointer should not change
+ Assert.Equal((nint) chain.Item1Ptr, (nint) chain.Head.PNext);
+ }
+
+ [Fact]
+ public unsafe void TestManagedChainReplaceMiddle()
+ {
+ using var chain = ManagedChain.Create
+ (
+ item1: new PhysicalDeviceDescriptorIndexingFeatures
+ {
+ // We can set any non-default values, note we do not need to set SType or PNext
+ // indeed they will be overwritten.
+ ShaderInputAttachmentArrayDynamicIndexing = true
+ }
+ );
+
+ // Ensure all STypes set correctly
+ Assert.Equal(StructureType.PhysicalDeviceFeatures2, chain.Head.SType);
+ Assert.Equal(StructureType.PhysicalDeviceDescriptorIndexingFeatures, chain.Item1.SType);
+ Assert.Equal(StructureType.PhysicalDeviceAccelerationStructureFeaturesKhr, chain.Item2.SType);
+
+ // Ensure pointers set correctly
+ Assert.Equal((nint) chain.Item1Ptr, (nint) chain.Head.PNext);
+ Assert.Equal((nint) chain.Item2Ptr, (nint) chain.Item1.PNext);
+ Assert.Equal(0, (nint) chain.Item2.PNext);
+
+ // Check our value was set
+ Assert.True(chain.Item1.ShaderInputAttachmentArrayDynamicIndexing);
+
+ var item1Ptr = chain.Item1Ptr;
+
+ // Overwrite Item1
+ chain.Item1 = new PhysicalDeviceDescriptorIndexingFeatures
+ {
+ // Again we do not need to set SType or PNext, which will be set to the correct values
+ ShaderInputAttachmentArrayDynamicIndexing = false
+ };
+
+ // Check our value was cleared
+ Assert.False(chain.Item1.ShaderInputAttachmentArrayDynamicIndexing);
+
+ // Note all the pointers are still correct (and have not changed)
+ Assert.Equal((nint) chain.Item1Ptr, (nint) chain.Head.PNext);
+ Assert.Equal((nint) chain.Item2Ptr, (nint) chain.Item1.PNext);
+ Assert.Equal(0, (nint) chain.Item2.PNext);
+
+ // As is the SType
+ Assert.Equal(StructureType.PhysicalDeviceDescriptorIndexingFeatures, chain.Item1.SType);
+ chain.Dispose();
+ }
+
+ [Fact]
+ public unsafe void TestManagedChainDuplicate()
+ {
+ using var chain = ManagedChain.Create
+ (
+ item1: new PhysicalDeviceDescriptorIndexingFeatures {ShaderInputAttachmentArrayDynamicIndexing = true}
+ );
+
+ // Ensure all STypes set correctly
+ Assert.Equal(StructureType.PhysicalDeviceFeatures2, chain.Head.SType);
+ Assert.Equal(StructureType.PhysicalDeviceDescriptorIndexingFeatures, chain.Item1.SType);
+
+ // Ensure pointers set correctly
+ Assert.Equal((nint) chain.Item1Ptr, (nint) chain.Head.PNext);
+ Assert.Equal(0, (nint) chain.Item1.PNext);
+
+ // Check flag set
+ Assert.True(chain.Item1.ShaderInputAttachmentArrayDynamicIndexing);
+
+ using var newChain = chain.Duplicate();
+
+ // Ensure all STypes set correctly
+ Assert.Equal(StructureType.PhysicalDeviceFeatures2, chain.Head.SType);
+ Assert.Equal(StructureType.PhysicalDeviceDescriptorIndexingFeatures, chain.Item1.SType);
+
+ // Ensure pointers set correctly
+ Assert.Equal((nint) chain.Item1Ptr, (nint) chain.Head.PNext);
+ Assert.Equal(0, (nint) chain.Item1.PNext);
+
+ // Check flag set
+ Assert.True(chain.Item1.ShaderInputAttachmentArrayDynamicIndexing);
+
+ // Check we have new copies
+ Assert.NotEqual((nint) chain.HeadPtr, (nint) newChain.HeadPtr);
+ Assert.NotEqual((nint) chain.Item1Ptr, (nint) newChain.Item1Ptr);
+
+ // Test equality
+ Assert.Equal(chain, newChain);
+ Assert.True(chain == newChain);
+
+ // Modify second chain
+ newChain.Item1 = default;
+
+ // Test equality
+ Assert.NotEqual(chain, newChain);
+ Assert.False(chain == newChain);
+ }
+
+ [Fact]
+ public unsafe void TestManagedChainAdd()
+ {
+ using var chain = ManagedChain.Create
+ (
+ item1: new PhysicalDeviceDescriptorIndexingFeatures {ShaderInputAttachmentArrayDynamicIndexing = true}
+ );
+
+ // Ensure all STypes set correctly
+ Assert.Equal(StructureType.PhysicalDeviceFeatures2, chain.Head.SType);
+ Assert.Equal(StructureType.PhysicalDeviceDescriptorIndexingFeatures, chain.Item1.SType);
+
+ // Ensure pointers set correctly
+ Assert.Equal((nint) chain.Item1Ptr, (nint) chain.Head.PNext);
+ Assert.Equal(0, (nint) chain.Item1.PNext);
+
+ // Check flag set
+ Assert.True(chain.Item1.ShaderInputAttachmentArrayDynamicIndexing);
+
+ using var newChain = chain.Add(default(PhysicalDeviceAccelerationStructureFeaturesKHR));
+
+ // Ensure all STypes set correctly
+ Assert.Equal(StructureType.PhysicalDeviceFeatures2, newChain.Head.SType);
+ Assert.Equal(StructureType.PhysicalDeviceDescriptorIndexingFeatures, newChain.Item1.SType);
+ Assert.Equal(StructureType.PhysicalDeviceAccelerationStructureFeaturesKhr, newChain.Item2.SType);
+
+ // Ensure pointers set correctly
+ Assert.Equal((nint) newChain.Item1Ptr, (nint) newChain.Head.PNext);
+ Assert.Equal((nint) newChain.Item2Ptr, (nint) newChain.Item1.PNext);
+ Assert.Equal(0, (nint) newChain.Item2.PNext);
+
+ // Check flag still set
+ Assert.True(newChain.Item1.ShaderInputAttachmentArrayDynamicIndexing);
+
+ // Check we have new copies
+ Assert.NotEqual((nint) chain.HeadPtr, (nint) newChain.HeadPtr);
+ Assert.NotEqual((nint) chain.Item1Ptr, (nint) newChain.Item1Ptr);
+ }
+
+ [Fact]
+ public unsafe void TestManagedChainTruncate()
+ {
+ using var chain =
+ ManagedChain.Create<
+ PhysicalDeviceFeatures2,
+ PhysicalDeviceDescriptorIndexingFeatures,
+ PhysicalDeviceAccelerationStructureFeaturesKHR>
+ (
+ item2: new PhysicalDeviceAccelerationStructureFeaturesKHR
+ {AccelerationStructure = true}
+ );
+
+ // Ensure all STypes set correctly
+ Assert.Equal(StructureType.PhysicalDeviceFeatures2, chain.Head.SType);
+ Assert.Equal(StructureType.PhysicalDeviceDescriptorIndexingFeatures, chain.Item1.SType);
+ Assert.Equal(StructureType.PhysicalDeviceAccelerationStructureFeaturesKhr, chain.Item2.SType);
+
+ // Ensure pointers set correctly
+ Assert.Equal((nint) chain.Item1Ptr, (nint) chain.Head.PNext);
+ Assert.Equal((nint) chain.Item2Ptr, (nint) chain.Item1.PNext);
+ Assert.Equal(0, (nint) chain.Item2.PNext);
+
+ // Check flag set
+ Assert.True(chain.Item2.AccelerationStructure);
+
+ using var newChain = chain.Truncate(out var accelerationStructure);
+
+ Assert.Equal(2, newChain.Count);
+
+ // Ensure all STypes set correctly
+ Assert.Equal(StructureType.PhysicalDeviceFeatures2, newChain.Head.SType);
+ Assert.Equal(StructureType.PhysicalDeviceDescriptorIndexingFeatures, newChain.Item1.SType);
+
+ // Ensure pointers set correctly
+ Assert.Equal((nint) newChain.Item1Ptr, (nint) newChain.Head.PNext);
+ Assert.Equal(0, (nint) newChain.Item1.PNext);
+
+ // Check removed type flag
+ Assert.True(accelerationStructure.AccelerationStructure);
+
+ // Check we have new copies
+ Assert.NotEqual((nint) chain.HeadPtr, (nint) newChain.HeadPtr);
+ Assert.NotEqual((nint) chain.Item1Ptr, (nint) newChain.Item1Ptr);
+ }
+
+ [Fact]
+ public unsafe void TestManagedChainLoad()
+ {
+ // Load an unmanaged chain
+ var indexingFeatures = new PhysicalDeviceDescriptorIndexingFeatures
+ {
+ ShaderInputAttachmentArrayDynamicIndexing = true
+ };
+ PhysicalDeviceFeatures2
+ .Chain(out var unmanagedChain)
+ .SetNext(ref indexingFeatures)
+ .AddNext(out PhysicalDeviceAccelerationStructureFeaturesKHR accelerationStructureFeaturesKhr);
+
+ // Loads a new managed chain from an unmanaged chain
+ using var managedChain =
+ ManagedChain.Load(out var errors, unmanagedChain);
+
+ // Check we had no loading errors
+ Assert.Equal("", errors);
+
+ // Check the flag still set
+ Assert.True(managedChain.Item1.ShaderInputAttachmentArrayDynamicIndexing);
+
+ // Ensure all STypes set correctly
+ Assert.Equal(StructureType.PhysicalDeviceFeatures2, managedChain.Head.SType);
+ Assert.Equal(StructureType.PhysicalDeviceDescriptorIndexingFeatures, managedChain.Item1.SType);
+ Assert.Equal(StructureType.PhysicalDeviceAccelerationStructureFeaturesKhr, managedChain.Item2.SType);
+
+ // Ensure pointers set correctly
+ Assert.Equal((nint) managedChain.Item1Ptr, (nint) managedChain.Head.PNext);
+ Assert.Equal((nint) managedChain.Item2Ptr, (nint) managedChain.Item1.PNext);
+ Assert.Equal(0, (nint) managedChain.Item2.PNext);
+ }
+
+ [Fact]
+ public void TestManagedChainLoadWithError()
+ {
+ var indexingFeatures = new PhysicalDeviceDescriptorIndexingFeatures
+ {
+ ShaderInputAttachmentArrayDynamicIndexing = true
+ };
+ // Load an unmanaged chain
+ DeviceCreateInfo
+ .Chain(out var unmanagedChain)
+ .AddNext(out PhysicalDeviceFeatures2 features2)
+ .SetNext(ref indexingFeatures)
+ .AddNext(out PhysicalDeviceAccelerationStructureFeaturesKHR accelerationStructureFeaturesKhr);
+
+ // Loads a new managed chain from an unmanaged chain
+ using var managedChain =
+ ManagedChain.Load<
+ DeviceCreateInfo,
+ // Note we are supplied a PhysicalDeviceFeatures2 here from the unmanaged chain
+ PhysicalDeviceAccelerationStructureFeaturesKHR,
+ PhysicalDeviceDescriptorIndexingFeatures,
+ PhysicalDeviceAccelerationStructureFeaturesKHR,
+ // Note that the unmanaged chain did not supply a 5th entry
+ PhysicalDeviceFeatures2>(out var errors, unmanagedChain);
+
+ // Check for errors
+ var errorsArray = errors.Split(new[] {'\r', '\n'}, StringSplitOptions.RemoveEmptyEntries);
+ Assert.Equal(2, errorsArray.Length);
+ Assert.Equal
+ (
+ "The unmanaged chain has a structure type PhysicalDeviceFeatures2Khr at position 2; expected PhysicalDeviceAccelerationStructureFeaturesKhr",
+ errorsArray[0]
+ );
+ Assert.Equal
+ (
+ "The unmanaged chain was length 4, expected length 5", errorsArray[1]
+ );
+
+ // Despite the errors indexing features was at the right location so was loaded
+ Assert.True(managedChain.Item2.ShaderInputAttachmentArrayDynamicIndexing);
+ }
+
+ [Fact]
+ public void TestManagedChainLoadWithErrorTooLong()
+ {
+ var indexingFeatures = new PhysicalDeviceDescriptorIndexingFeatures
+ {
+ ShaderInputAttachmentArrayDynamicIndexing = true
+ };
+ // Load an unmanaged chain
+ DeviceCreateInfo
+ .Chain(out var unmanagedChain)
+ .AddNext(out PhysicalDeviceFeatures2 features2)
+ .SetNext(ref indexingFeatures)
+ .AddNext(out PhysicalDeviceAccelerationStructureFeaturesKHR accelerationStructureFeaturesKhr);
+
+ // Try loading a shorter managed chain
+ using var managedChain =
+ ManagedChain.Load<
+ DeviceCreateInfo,
+ PhysicalDeviceFeatures2,
+ PhysicalDeviceDescriptorIndexingFeatures>(out var errors, unmanagedChain);
+
+ // Check for errors
+ Assert.Equal(@"The unmanaged chain was longer than the expected length 3", errors);
+
+ // Despite the errors indexing features was at the right location so was loaded
+ Assert.True(managedChain.Item2.ShaderInputAttachmentArrayDynamicIndexing);
+ }
+
+ [Fact]
+ public void TestReadOnlyList()
+ {
+ using var chain = ManagedChain.Create();
+
+ Assert.Equal(3, chain.Count);
+
+ // Ensure all STypes set correctly using indexer
+ Assert.Equal(StructureType.PhysicalDeviceFeatures2, chain[0].StructureType());
+ Assert.Equal(StructureType.PhysicalDeviceDescriptorIndexingFeatures, chain[1].StructureType());
+ Assert.Equal(StructureType.PhysicalDeviceAccelerationStructureFeaturesKhr, chain[2].StructureType());
+
+ Assert.Throws(() => chain[3]);
+
+ // Get array using IEnumerable implementation
+ var structures = chain.ToArray();
+
+ // Check concrete types
+ Assert.IsType(structures[0]);
+ Assert.IsType(structures[1]);
+ Assert.IsType(structures[2]);
+ }
+
+ [Fact]
+ public void TestDeconstructor()
+ {
+ using var chain = ManagedChain.Create();
+
+ var (physicalDeviceFeatures2, indexingFeatures, accelerationStructureFeaturesKhr) = chain;
+
+ // Ensure all STypes set correctly
+ Assert.Equal(StructureType.PhysicalDeviceFeatures2, physicalDeviceFeatures2.SType);
+ Assert.Equal(StructureType.PhysicalDeviceDescriptorIndexingFeatures, indexingFeatures.SType);
+ Assert.Equal
+ (StructureType.PhysicalDeviceAccelerationStructureFeaturesKhr, accelerationStructureFeaturesKhr.SType);
+ }
+
+ [Fact]
+ public unsafe void TestManagedChainGetHashCode()
+ {
+ using var chain = ManagedChain.Create
+ (
+ item1: new PhysicalDeviceDescriptorIndexingFeatures {ShaderInputAttachmentArrayDynamicIndexing = true}
+ );
+
+ // Ensure all STypes set correctly
+ Assert.Equal(StructureType.PhysicalDeviceFeatures2, chain.Head.SType);
+ Assert.Equal(StructureType.PhysicalDeviceDescriptorIndexingFeatures, chain.Item1.SType);
+
+ // Ensure pointers set correctly
+ Assert.Equal((nint) chain.Item1Ptr, (nint) chain.Head.PNext);
+ Assert.Equal(0, (nint) chain.Item1.PNext);
+
+ // Check flag set
+ Assert.True(chain.Item1.ShaderInputAttachmentArrayDynamicIndexing);
+
+ using var newChain = chain.Duplicate();
+
+ // Ensure all STypes set correctly
+ Assert.Equal(StructureType.PhysicalDeviceFeatures2, chain.Head.SType);
+ Assert.Equal(StructureType.PhysicalDeviceDescriptorIndexingFeatures, chain.Item1.SType);
+
+ Assert.Equal(StructureType.PhysicalDeviceDescriptorIndexingFeatures, chain.Item1Ptr->SType);
+
+ // Ensure pointers set correctly
+ Assert.Equal((nint) chain.Item1Ptr, (nint) chain.Head.PNext);
+ Assert.Equal(0, (nint) chain.Item1.PNext);
+
+ // Check flag set
+ Assert.True(chain.Item1.ShaderInputAttachmentArrayDynamicIndexing);
+
+ // Check we have new copies
+ Assert.NotEqual((nint) chain.HeadPtr, (nint) newChain.HeadPtr);
+ Assert.NotEqual((nint) chain.Item1Ptr, (nint) newChain.Item1Ptr);
+
+ // Test equality
+ Assert.Equal(chain, newChain);
+ Assert.True(chain == newChain);
+ var hashCode = chain.GetHashCode();
+ var newHashCode = newChain.GetHashCode();
+ Assert.Equal(hashCode, newHashCode);
+
+ // Modify second chain
+ newChain.Item1 = default;
+
+ // Test equality
+ Assert.NotEqual(chain, newChain);
+ Assert.False(chain == newChain);
+ Assert.Equal(hashCode, chain.GetHashCode());
+ var newHashCode2 = newChain.GetHashCode();
+ Assert.NotEqual(hashCode, newHashCode2);
+ Assert.NotEqual(newHashCode, newHashCode2);
+ }
+}
diff --git a/src/Lab/Experiments/PrototypeStructChaining/PrototypeStructChaining.Test/TestManagedChainsAny.cs b/src/Lab/Experiments/PrototypeStructChaining/PrototypeStructChaining.Test/TestManagedChainsAny.cs
new file mode 100644
index 0000000000..7b6c69018b
--- /dev/null
+++ b/src/Lab/Experiments/PrototypeStructChaining/PrototypeStructChaining.Test/TestManagedChainsAny.cs
@@ -0,0 +1,193 @@
+using System;
+using System.Linq;
+using Silk.NET.Vulkan;
+using Xunit;
+
+namespace PrototypeStructChaining.Test;
+
+public class TestManagedChainsAny
+{
+ [Fact]
+ public unsafe void TestManagedChainAny()
+ {
+ using var chain = ManagedChain.CreateAny
+ (
+ default(PhysicalDeviceFeatures2),
+ default(PhysicalDeviceDescriptorIndexingFeatures),
+ default(DeviceCreateInfo)
+ );
+
+ // Ensure all STypes set correctly
+ Assert.Equal(StructureType.PhysicalDeviceFeatures2, chain.Head.SType);
+ Assert.Equal(StructureType.PhysicalDeviceDescriptorIndexingFeatures, chain.Item1.SType);
+ Assert.Equal(StructureType.DeviceCreateInfo, chain.Item2.SType);
+
+ // Ensure pointers set correctly
+ Assert.Equal((nint) chain.Item1Ptr, (nint) chain.Head.PNext);
+ Assert.Equal((nint) chain.Item2Ptr, (nint) chain.Item1.PNext);
+ Assert.Equal(0, (nint) chain.Item2.PNext);
+ }
+
+ [Fact]
+ public unsafe void TestManagedChainDuplicateAny()
+ {
+ using var chain = ManagedChain.CreateAny
+ (
+ item1: new DeviceCreateInfo {Flags = 1U}
+ );
+
+ // Ensure all STypes set correctly
+ Assert.Equal(StructureType.PhysicalDeviceFeatures2, chain.Head.SType);
+ Assert.Equal(StructureType.DeviceCreateInfo, chain.Item1.SType);
+
+ // Ensure pointers set correctly
+ Assert.Equal((nint) chain.Item1Ptr, (nint) chain.Head.PNext);
+ Assert.Equal(0, (nint) chain.Item1.PNext);
+
+ // Check flag set
+ Assert.Equal(1U, chain.Item1.Flags);
+
+ using var newChain = chain.DuplicateAny();
+
+ // Ensure all STypes set correctly
+ Assert.Equal(StructureType.PhysicalDeviceFeatures2, chain.Head.SType);
+ Assert.Equal(StructureType.DeviceCreateInfo, chain.Item1.SType);
+
+ // Ensure pointers set correctly
+ Assert.Equal((nint) chain.Item1Ptr, (nint) chain.Head.PNext);
+ Assert.Equal(0, (nint) chain.Item1.PNext);
+
+ // Check flag set
+ Assert.Equal(1U, chain.Item1.Flags);
+
+ // Check we have new copies
+ Assert.NotEqual((nint) chain.HeadPtr, (nint) newChain.HeadPtr);
+ Assert.NotEqual((nint) chain.Item1Ptr, (nint) newChain.Item1Ptr);
+
+ // Test equality
+ Assert.Equal(chain, newChain);
+ Assert.True(chain == newChain);
+ }
+
+ [Fact]
+ public unsafe void TestManagedChainAddAny()
+ {
+ using var chain = ManagedChain.Create
+ (
+ item1: new PhysicalDeviceDescriptorIndexingFeatures {ShaderInputAttachmentArrayDynamicIndexing = true}
+ );
+
+ // Ensure all STypes set correctly
+ Assert.Equal(StructureType.PhysicalDeviceFeatures2, chain.Head.SType);
+ Assert.Equal(StructureType.PhysicalDeviceDescriptorIndexingFeatures, chain.Item1.SType);
+
+ // Ensure pointers set correctly
+ Assert.Equal((nint) chain.Item1Ptr, (nint) chain.Head.PNext);
+ Assert.Equal(0, (nint) chain.Item1.PNext);
+
+ // Check flag set
+ Assert.True(chain.Item1.ShaderInputAttachmentArrayDynamicIndexing);
+
+ using var newChain = chain.AddAny(default(DeviceCreateInfo));
+
+ // Ensure all STypes set correctly
+ Assert.Equal(StructureType.PhysicalDeviceFeatures2, newChain.Head.SType);
+ Assert.Equal(StructureType.PhysicalDeviceDescriptorIndexingFeatures, newChain.Item1.SType);
+ Assert.Equal(StructureType.DeviceCreateInfo, newChain.Item2.SType);
+
+ // Ensure pointers set correctly
+ Assert.Equal((nint) newChain.Item1Ptr, (nint) newChain.Head.PNext);
+ Assert.Equal((nint) newChain.Item2Ptr, (nint) newChain.Item1.PNext);
+ Assert.Equal(0, (nint) newChain.Item2.PNext);
+
+ // Check flag still set
+ Assert.True(newChain.Item1.ShaderInputAttachmentArrayDynamicIndexing);
+
+ // Check we have new copies
+ Assert.NotEqual((nint) chain.HeadPtr, (nint) newChain.HeadPtr);
+ Assert.NotEqual((nint) chain.Item1Ptr, (nint) newChain.Item1Ptr);
+ }
+
+ [Fact]
+ public unsafe void TestManagedChainTruncateAny()
+ {
+ using var chain =
+ ManagedChain.CreateAny<
+ PhysicalDeviceFeatures2,
+ PhysicalDeviceDescriptorIndexingFeatures,
+ DeviceCreateInfo>
+ (
+ item2: new DeviceCreateInfo {Flags = 1U}
+ );
+
+ // Ensure all STypes set correctly
+ Assert.Equal(StructureType.PhysicalDeviceFeatures2, chain.Head.SType);
+ Assert.Equal(StructureType.PhysicalDeviceDescriptorIndexingFeatures, chain.Item1.SType);
+ Assert.Equal(StructureType.DeviceCreateInfo, chain.Item2.SType);
+
+ // Ensure pointers set correctly
+ Assert.Equal((nint) chain.Item1Ptr, (nint) chain.Head.PNext);
+ Assert.Equal((nint) chain.Item2Ptr, (nint) chain.Item1.PNext);
+ Assert.Equal(0, (nint) chain.Item2.PNext);
+
+ // Check flag set
+ Assert.Equal(1U, chain.Item2.Flags);
+
+ using var newChain = chain.TruncateAny(out var deviceCreateInfo);
+
+ Assert.Equal(2, newChain.Count);
+
+ // Ensure all STypes set correctly
+ Assert.Equal(StructureType.PhysicalDeviceFeatures2, newChain.Head.SType);
+ Assert.Equal(StructureType.PhysicalDeviceDescriptorIndexingFeatures, newChain.Item1.SType);
+
+ // Ensure pointers set correctly
+ Assert.Equal((nint) newChain.Item1Ptr, (nint) newChain.Head.PNext);
+ Assert.Equal(0, (nint) newChain.Item1.PNext);
+
+ // Check removed type flag
+ Assert.Equal(1U, deviceCreateInfo.Flags);
+
+ // Check we have new copies
+ Assert.NotEqual((nint) chain.HeadPtr, (nint) newChain.HeadPtr);
+ Assert.NotEqual((nint) chain.Item1Ptr, (nint) newChain.Item1Ptr);
+
+ // NOTE: As the new chain is valid, we can use Truncate on it
+ using var finalChain = newChain.Truncate(out var indexingFeatures);
+ }
+
+ [Fact]
+ public unsafe void TestManagedChainLoadAny()
+ {
+ // Load an unmanaged chain
+ var indexingFeatures = new PhysicalDeviceDescriptorIndexingFeatures
+ {
+ ShaderInputAttachmentArrayDynamicIndexing = true
+ };
+ PhysicalDeviceFeatures2
+ .Chain(out var unmanagedChain)
+ .SetNext(ref indexingFeatures)
+ .AddNextAny(out DeviceCreateInfo deviceCreateInfo);
+
+ // Loads a new managed chain from an unmanaged chain
+ using var managedChain =
+ ManagedChain.LoadAny(out var errors, unmanagedChain);
+
+ // Check we had no loading errors
+ Assert.Equal("", errors);
+
+ // Check the flag still set
+ Assert.True(managedChain.Item1.ShaderInputAttachmentArrayDynamicIndexing);
+
+ // Ensure all STypes set correctly
+ Assert.Equal(StructureType.PhysicalDeviceFeatures2, managedChain.Head.SType);
+ Assert.Equal(StructureType.PhysicalDeviceDescriptorIndexingFeatures, managedChain.Item1.SType);
+ Assert.Equal(StructureType.DeviceCreateInfo, managedChain.Item2.SType);
+
+ // Ensure pointers set correctly
+ Assert.Equal((nint) managedChain.Item1Ptr, (nint) managedChain.Head.PNext);
+ Assert.Equal((nint) managedChain.Item2Ptr, (nint) managedChain.Item1.PNext);
+ Assert.Equal(0, (nint) managedChain.Item2.PNext);
+ }
+}
diff --git a/src/Lab/Experiments/PrototypeStructChaining/PrototypeStructChaining/ChainExtensions.cs b/src/Lab/Experiments/PrototypeStructChaining/PrototypeStructChaining/ChainExtensions.cs
new file mode 100644
index 0000000000..604c4ae29f
--- /dev/null
+++ b/src/Lab/Experiments/PrototypeStructChaining/PrototypeStructChaining/ChainExtensions.cs
@@ -0,0 +1,200 @@
+using System.Runtime.CompilerServices;
+
+namespace Silk.NET.Vulkan;
+
+public static class Chain
+{
+ ///
+ /// Provides a set of all the s that can be extended by a .
+ ///
+ public static readonly IReadOnlyDictionary> Extensions =
+ new Dictionary>
+ {
+ [Vulkan.StructureType.PhysicalDeviceFeatures2] = new HashSet
+ {
+ Vulkan.StructureType.DeviceCreateInfo
+ },
+ [Vulkan.StructureType.PhysicalDeviceDescriptorIndexingFeatures] = new HashSet
+ {
+ Vulkan.StructureType.PhysicalDeviceFeatures2,
+ Vulkan.StructureType.DeviceCreateInfo
+ },
+ [Vulkan.StructureType.PhysicalDeviceAccelerationStructureFeaturesKhr] = new HashSet
+ {
+ Vulkan.StructureType.PhysicalDeviceFeatures2,
+ Vulkan.StructureType.DeviceCreateInfo
+ }
+ };
+
+ ///
+ /// Provides a set of all the s that can extend a .
+ ///
+ public static readonly IReadOnlyDictionary> Extenders =
+ new Dictionary>
+ {
+ [Vulkan.StructureType.DeviceCreateInfo] = new HashSet
+ {
+ Vulkan.StructureType.PhysicalDeviceFeatures2,
+ Vulkan.StructureType.PhysicalDeviceDescriptorIndexingFeatures,
+ Vulkan.StructureType.PhysicalDeviceAccelerationStructureFeaturesKhr
+ },
+ [Vulkan.StructureType.PhysicalDeviceFeatures2] = new HashSet
+ {
+ Vulkan.StructureType.PhysicalDeviceDescriptorIndexingFeatures,
+ Vulkan.StructureType.PhysicalDeviceAccelerationStructureFeaturesKhr
+ }
+ };
+
+ ///
+ /// Provides a mapping from the to the corresponding .
+ ///
+ public static readonly IReadOnlyDictionary ClrTypes =
+ new Dictionary
+ {
+ [Vulkan.StructureType.DeviceCreateInfo] = typeof(DeviceCreateInfo),
+ [Vulkan.StructureType.PhysicalDeviceFeatures2] = typeof(PhysicalDeviceFeatures2),
+ [Vulkan.StructureType.PhysicalDeviceDescriptorIndexingFeatures] =
+ typeof(PhysicalDeviceDescriptorIndexingFeatures),
+ [Vulkan.StructureType.PhysicalDeviceAccelerationStructureFeaturesKhr] =
+ typeof(PhysicalDeviceAccelerationStructureFeaturesKHR)
+ };
+
+ ///
+ /// Provides a mapping from the