diff --git a/src/ComputeSharp.D2D1.SourceGenerators/ComputeSharp.D2D1.SourceGenerators.csproj b/src/ComputeSharp.D2D1.SourceGenerators/ComputeSharp.D2D1.SourceGenerators.csproj
index 11887f91e..d8fa14633 100644
--- a/src/ComputeSharp.D2D1.SourceGenerators/ComputeSharp.D2D1.SourceGenerators.csproj
+++ b/src/ComputeSharp.D2D1.SourceGenerators/ComputeSharp.D2D1.SourceGenerators.csproj
@@ -68,6 +68,8 @@
+
+
diff --git a/src/ComputeSharp.D2D1/Attributes/Enums/D2D1CompileOptions.cs b/src/ComputeSharp.D2D1/Attributes/Enums/D2D1CompileOptions.cs
index 0f3bc5598..976d2935c 100644
--- a/src/ComputeSharp.D2D1/Attributes/Enums/D2D1CompileOptions.cs
+++ b/src/ComputeSharp.D2D1/Attributes/Enums/D2D1CompileOptions.cs
@@ -2,6 +2,7 @@
#if SOURCE_GENERATOR
using D3DCOMPILE = Windows.Win32.PInvoke;
#else
+using System.Diagnostics.CodeAnalysis;
using ComputeSharp.D2D1.Interop;
using ComputeSharp.Win32;
@@ -155,6 +156,34 @@ public enum D2D1CompileOptions
///
WarningsAreErrors = (int)D3DCOMPILE.D3DCOMPILE_WARNINGS_ARE_ERRORS,
+ ///
+ /// Declares minimum precision support in the compiled bytecode, which can allow Direct2D to link the
+ /// resulting effect. This flag has no effect unless is also set.
+ ///
+ ///
+ ///
+ /// Setting embeds an export function in the compiled bytecode, but Direct2D
+ /// will generally still not link effects created from it. Additionally declaring minimum precision support
+ /// has been observed to make linking engage, which can remove one rendering pass, and the intermediate
+ /// surface that goes with it, for each effect that ends up being linked.
+ ///
+ ///
+ /// Specifically, this flag appends a shader feature info blob declaring
+ /// D3D_SHADER_FEATURE_MINIMUM_PRECISION to the compiled bytecode. The compiled shader instructions
+ /// are left untouched, so no computation is lowered to a reduced precision. Note that Direct2D may still
+ /// run the bytecode through a minimum precision conversion, depending on the target being rendered to.
+ ///
+ ///
+ /// This relies on behavior that is neither documented nor guaranteed, and that may stop working at any
+ /// time. It should only be used after measuring that it actually improves performance, and shaders using
+ /// it should be validated to still produce correct results.
+ ///
+ ///
+#if !SOURCE_GENERATOR
+ [Experimental("CMPSEXP0001", UrlFormat = "https://github.com/Sergio0694/ComputeSharp")]
+#endif
+ DeclareMinimumPrecisionSupport = 1 << 29,
+
///
/// Strips the reflection data from the generated shader bytecode. The bytecode size will be smaller, but trying
/// to perform reflection on the shader will return inaccurate results. It is recommend if not reflection is used.
diff --git a/src/ComputeSharp.D2D1/Shaders/Interop/D2D1ShaderCompiler.cs b/src/ComputeSharp.D2D1/Shaders/Interop/D2D1ShaderCompiler.cs
index 8a8400b82..b31ab209a 100644
--- a/src/ComputeSharp.D2D1/Shaders/Interop/D2D1ShaderCompiler.cs
+++ b/src/ComputeSharp.D2D1/Shaders/Interop/D2D1ShaderCompiler.cs
@@ -4,6 +4,9 @@
using ComputeSharp.D2D1.Shaders.Translation;
using ComputeSharp.Win32;
+// The compiler is responsible for implementing DeclareMinimumPrecisionSupport, so it has to reference it
+#pragma warning disable CMPSEXP0001
+
namespace ComputeSharp.D2D1.Interop;
///
@@ -68,10 +71,12 @@ public static unsafe ReadOnlyMemory Compile(
// Check the additional compile options (not provided by FXC directly)
bool enableLinking = (options & D2D1CompileOptions.EnableLinking) == D2D1CompileOptions.EnableLinking;
bool stripReflectionData = (options & D2D1CompileOptions.StripReflectionData) == D2D1CompileOptions.StripReflectionData;
+ bool declareMinimumPrecisionSupport = (options & D2D1CompileOptions.DeclareMinimumPrecisionSupport) == D2D1CompileOptions.DeclareMinimumPrecisionSupport;
// Remove the additional options to make them blittable to flags
options &= ~D2D1CompileOptions.EnableLinking;
options &= ~D2D1CompileOptions.StripReflectionData;
+ options &= ~D2D1CompileOptions.DeclareMinimumPrecisionSupport;
// Compile the standalone D2D1 full shader
using ComPtr d3DBlobFullShader = D3DCompiler.Compile(
@@ -102,7 +107,9 @@ public static unsafe ReadOnlyMemory Compile(
stripReflectionData: stripReflectionData);
// Embed it as private data if requested
- using ComPtr d3DBlobLinked = D3DCompiler.SetD3DPrivateData(d3DBlobFullShader.Get(), d3DBlobFunction.Get());
+ using ComPtr d3DBlobLinked = D3DCompiler.SetD3DPrivateData(
+ shaderBytecode: D3DCompiler.GetBytecode(d3DBlobFullShader.Get(), declareMinimumPrecisionSupport),
+ exportBlob: d3DBlobFunction.Get());
void* blobLinkedPtr = d3DBlobLinked.Get()->GetBufferPointer();
nuint blobLinkedSize = d3DBlobLinked.Get()->GetBufferSize();
diff --git a/src/ComputeSharp.D2D1/Shaders/Translation/D3DCompiler.cs b/src/ComputeSharp.D2D1/Shaders/Translation/D3DCompiler.cs
index 5a615899a..0adf8d5af 100644
--- a/src/ComputeSharp.D2D1/Shaders/Translation/D3DCompiler.cs
+++ b/src/ComputeSharp.D2D1/Shaders/Translation/D3DCompiler.cs
@@ -16,6 +16,9 @@
using ComputeSharp.Win32;
#endif
+// The compiler is responsible for implementing DeclareMinimumPrecisionSupport, so it has to reference it
+#pragma warning disable CMPSEXP0001
+
namespace ComputeSharp.D2D1.Shaders.Translation;
///
@@ -55,9 +58,11 @@ public static ComPtr Compile(
bool enableLinking = (options & D2D1CompileOptions.EnableLinking) == D2D1CompileOptions.EnableLinking;
bool stripReflectionData = (options & D2D1CompileOptions.StripReflectionData) == D2D1CompileOptions.StripReflectionData;
+ bool declareMinimumPrecisionSupport = (options & D2D1CompileOptions.DeclareMinimumPrecisionSupport) == D2D1CompileOptions.DeclareMinimumPrecisionSupport;
options &= ~D2D1CompileOptions.EnableLinking;
options &= ~D2D1CompileOptions.StripReflectionData;
+ options &= ~D2D1CompileOptions.DeclareMinimumPrecisionSupport;
try
{
@@ -95,7 +100,9 @@ public static ComPtr Compile(
stripReflectionData: stripReflectionData);
// Embed it as private data if requested
- using ComPtr d3DBlobLinked = SetD3DPrivateData(d3DBlobFullShader.Get(), d3DBlobFunction.Get());
+ using ComPtr d3DBlobLinked = SetD3DPrivateData(
+ shaderBytecode: GetBytecode(d3DBlobFullShader.Get(), declareMinimumPrecisionSupport),
+ exportBlob: d3DBlobFunction.Get());
return d3DBlobLinked.Move();
}
@@ -220,29 +227,50 @@ public static ComPtr Compile(
}
///
- /// Embeds the bytecode for an exported shader as private data into another shader bytecode.
+ /// Gets the bytecode of a full shader, optionally declaring minimum precision support in it.
///
/// The bytecode for the full shader.
- /// The bytecode for the shader function to export.
- /// An instance with the combined data of and .
- public static ComPtr SetD3DPrivateData(ID3DBlob* shaderBlob, ID3DBlob* exportBlob)
+ /// Whether to declare minimum precision support in the bytecode.
+ /// The bytecode for the full shader.
+ ///
+ /// Patched bytecode is a complete, valid DXBC container with an updated checksum.
+ ///
+ public static ReadOnlySpan GetBytecode(ID3DBlob* shaderBlob, bool declareMinimumPrecisionSupport)
{
- void* shaderPtr = shaderBlob->GetBufferPointer();
- nuint shaderSize = shaderBlob->GetBufferSize();
+ ReadOnlySpan bytecode = new(shaderBlob->GetBufferPointer(), (int)shaderBlob->GetBufferSize());
+
+ if (declareMinimumPrecisionSupport)
+ {
+ return Dxbc.CreateWithMinimumPrecisionShaderFeatureFlag(bytecode);
+ }
+
+ return bytecode;
+ }
+ ///
+ /// Embeds the bytecode for an exported shader as private data into another shader bytecode.
+ ///
+ /// The bytecode for the full shader.
+ /// The bytecode for the shader function to export.
+ /// An instance with the combined data of and .
+ public static ComPtr SetD3DPrivateData(ReadOnlySpan shaderBytecode, ID3DBlob* exportBlob)
+ {
void* exportPtr = exportBlob->GetBufferPointer();
nuint exportSize = exportBlob->GetBufferSize();
using ComPtr resultBlob = default;
- DirectX.D3DSetBlobPart(
- pSrcData: shaderPtr,
- SrcDataSize: shaderSize,
- Part: D3D_BLOB_PART.D3D_BLOB_PRIVATE_DATA,
- Flags: 0,
- pPart: exportPtr,
- PartSize: exportSize,
- ppNewShader: resultBlob.GetAddressOf()).Assert();
+ fixed (byte* shaderPtr = shaderBytecode)
+ {
+ DirectX.D3DSetBlobPart(
+ pSrcData: shaderPtr,
+ SrcDataSize: (nuint)shaderBytecode.Length,
+ Part: D3D_BLOB_PART.D3D_BLOB_PRIVATE_DATA,
+ Flags: 0,
+ pPart: exportPtr,
+ PartSize: exportSize,
+ ppNewShader: resultBlob.GetAddressOf()).Assert();
+ }
return resultBlob.Move();
}
diff --git a/src/ComputeSharp.D2D1/Shaders/Translation/Dxbc.Checksum.cs b/src/ComputeSharp.D2D1/Shaders/Translation/Dxbc.Checksum.cs
new file mode 100644
index 000000000..d62dae716
--- /dev/null
+++ b/src/ComputeSharp.D2D1/Shaders/Translation/Dxbc.Checksum.cs
@@ -0,0 +1,275 @@
+using System;
+using System.Buffers.Binary;
+
+namespace ComputeSharp.D2D1.Shaders.Translation;
+
+///
+partial class Dxbc
+{
+ ///
+ /// The offset of the checksum in a DXBC container.
+ ///
+ private const int ChecksumOffset = 4;
+
+ ///
+ /// The size of the checksum in a DXBC container.
+ ///
+ private const int ChecksumSize = 16;
+
+ ///
+ /// Recomputes the checksum of a DXBC container in place.
+ ///
+ /// The DXBC container to update.
+ ///
+ /// The checksum covers the whole container except for the signature and the checksum itself.
+ ///
+ public static void UpdateChecksum(Span bytecode)
+ {
+ Span state = stackalloc uint[4] { 0x67452301, 0xEFCDAB89, 0x98BADCFE, 0x10325476 };
+
+ ComputeHash(bytecode.Slice(ChecksumOffset + ChecksumSize), state);
+
+ for (int i = 0; i < state.Length; i++)
+ {
+ BinaryPrimitives.WriteUInt32LittleEndian(bytecode.Slice(ChecksumOffset + (i * sizeof(uint))), state[i]);
+ }
+ }
+
+ ///
+ /// Computes the hash used by DXBC containers over some input data.
+ ///
+ /// The data to hash.
+ /// The hash state to initialize and update.
+ ///
+ ///
+ /// This is the MD5 algorithm from RFC 1321 with a modified handling of the final block (or blocks): the
+ /// length in bits is stored at the start of that block rather than at offset 56, the data is shifted by
+ /// four bytes to make room for it, and the last four bytes hold 1 | (byteCount << 1) rather
+ /// than the high half of the length. The transform itself is unchanged.
+ ///
+ ///
+ /// For more info, see .
+ ///
+ ///
+ private static void ComputeHash(ReadOnlySpan data, Span state)
+ {
+ int byteCount = data.Length;
+ int leftOver = byteCount & 0x3F;
+ int padAmount;
+ bool hasTwoRowsOfPadding;
+
+ // The data is padded so that the final block has room for the trailing length
+ if (leftOver < 56)
+ {
+ padAmount = 56 - leftOver;
+ hasTwoRowsOfPadding = false;
+ }
+ else
+ {
+ padAmount = 120 - leftOver;
+ hasTwoRowsOfPadding = true;
+ }
+
+ int blockCount = (byteCount + padAmount + 8) >> 6;
+ int nextEndState = hasTwoRowsOfPadding ? blockCount - 2 : blockCount - 1;
+
+ Span block = stackalloc byte[64];
+ Span x = stackalloc uint[16];
+
+ for (int i = 0, offset = 0; i < blockCount; i++, offset += 64)
+ {
+ if (i == nextEndState)
+ {
+ int remainder = byteCount - offset;
+
+ // The padding is a single 0x80 byte followed by zeros, so clearing the block
+ // upfront means only that first byte ever has to be written explicitly.
+ block.Clear();
+
+ if (!hasTwoRowsOfPadding && i == blockCount - 1)
+ {
+ BinaryPrimitives.WriteUInt32LittleEndian(block, (uint)byteCount << 3);
+ data.Slice(offset, remainder).CopyTo(block.Slice(sizeof(uint)));
+
+ block[sizeof(uint) + remainder] = 0x80;
+
+ BinaryPrimitives.WriteUInt32LittleEndian(block.Slice(60), 1u | ((uint)byteCount << 1));
+ }
+ else if (i == blockCount - 2)
+ {
+ data.Slice(offset, remainder).CopyTo(block);
+
+ block[remainder] = 0x80;
+
+ nextEndState = blockCount - 1;
+ }
+ else
+ {
+ // The 0x80 byte was already written at the end of the previous block,
+ // so the rest of the padding in this one is just the zeros from above.
+ BinaryPrimitives.WriteUInt32LittleEndian(block, (uint)byteCount << 3);
+ BinaryPrimitives.WriteUInt32LittleEndian(block.Slice(60), 1u | ((uint)byteCount << 1));
+ }
+
+ LoadBlock(block, x);
+ }
+ else
+ {
+ LoadBlock(data.Slice(offset, 64), x);
+ }
+
+ Transform(state, x);
+ }
+ }
+
+ ///
+ /// Loads a 64 bytes block into the 16 words used by a single transform.
+ ///
+ /// The block to load.
+ /// The resulting words.
+ private static void LoadBlock(ReadOnlySpan block, Span x)
+ {
+ for (int i = 0; i < x.Length; i++)
+ {
+ x[i] = BinaryPrimitives.ReadUInt32LittleEndian(block.Slice(i * sizeof(uint)));
+ }
+ }
+
+ ///
+ /// Applies the four MD5 rounds for a single block to the hash state.
+ ///
+ /// The hash state to update.
+ /// The 16 words of the block being processed.
+ private static void Transform(Span state, ReadOnlySpan x)
+ {
+ uint a = state[0];
+ uint b = state[1];
+ uint c = state[2];
+ uint d = state[3];
+
+ // Round 1
+ FF(ref a, b, c, d, x[0], 7, 0xD76AA478);
+ FF(ref d, a, b, c, x[1], 12, 0xE8C7B756);
+ FF(ref c, d, a, b, x[2], 17, 0x242070DB);
+ FF(ref b, c, d, a, x[3], 22, 0xC1BDCEEE);
+ FF(ref a, b, c, d, x[4], 7, 0xF57C0FAF);
+ FF(ref d, a, b, c, x[5], 12, 0x4787C62A);
+ FF(ref c, d, a, b, x[6], 17, 0xA8304613);
+ FF(ref b, c, d, a, x[7], 22, 0xFD469501);
+ FF(ref a, b, c, d, x[8], 7, 0x698098D8);
+ FF(ref d, a, b, c, x[9], 12, 0x8B44F7AF);
+ FF(ref c, d, a, b, x[10], 17, 0xFFFF5BB1);
+ FF(ref b, c, d, a, x[11], 22, 0x895CD7BE);
+ FF(ref a, b, c, d, x[12], 7, 0x6B901122);
+ FF(ref d, a, b, c, x[13], 12, 0xFD987193);
+ FF(ref c, d, a, b, x[14], 17, 0xA679438E);
+ FF(ref b, c, d, a, x[15], 22, 0x49B40821);
+
+ // Round 2
+ GG(ref a, b, c, d, x[1], 5, 0xF61E2562);
+ GG(ref d, a, b, c, x[6], 9, 0xC040B340);
+ GG(ref c, d, a, b, x[11], 14, 0x265E5A51);
+ GG(ref b, c, d, a, x[0], 20, 0xE9B6C7AA);
+ GG(ref a, b, c, d, x[5], 5, 0xD62F105D);
+ GG(ref d, a, b, c, x[10], 9, 0x02441453);
+ GG(ref c, d, a, b, x[15], 14, 0xD8A1E681);
+ GG(ref b, c, d, a, x[4], 20, 0xE7D3FBC8);
+ GG(ref a, b, c, d, x[9], 5, 0x21E1CDE6);
+ GG(ref d, a, b, c, x[14], 9, 0xC33707D6);
+ GG(ref c, d, a, b, x[3], 14, 0xF4D50D87);
+ GG(ref b, c, d, a, x[8], 20, 0x455A14ED);
+ GG(ref a, b, c, d, x[13], 5, 0xA9E3E905);
+ GG(ref d, a, b, c, x[2], 9, 0xFCEFA3F8);
+ GG(ref c, d, a, b, x[7], 14, 0x676F02D9);
+ GG(ref b, c, d, a, x[12], 20, 0x8D2A4C8A);
+
+ // Round 3
+ HH(ref a, b, c, d, x[5], 4, 0xFFFA3942);
+ HH(ref d, a, b, c, x[8], 11, 0x8771F681);
+ HH(ref c, d, a, b, x[11], 16, 0x6D9D6122);
+ HH(ref b, c, d, a, x[14], 23, 0xFDE5380C);
+ HH(ref a, b, c, d, x[1], 4, 0xA4BEEA44);
+ HH(ref d, a, b, c, x[4], 11, 0x4BDECFA9);
+ HH(ref c, d, a, b, x[7], 16, 0xF6BB4B60);
+ HH(ref b, c, d, a, x[10], 23, 0xBEBFBC70);
+ HH(ref a, b, c, d, x[13], 4, 0x289B7EC6);
+ HH(ref d, a, b, c, x[0], 11, 0xEAA127FA);
+ HH(ref c, d, a, b, x[3], 16, 0xD4EF3085);
+ HH(ref b, c, d, a, x[6], 23, 0x04881D05);
+ HH(ref a, b, c, d, x[9], 4, 0xD9D4D039);
+ HH(ref d, a, b, c, x[12], 11, 0xE6DB99E5);
+ HH(ref c, d, a, b, x[15], 16, 0x1FA27CF8);
+ HH(ref b, c, d, a, x[2], 23, 0xC4AC5665);
+
+ // Round 4
+ II(ref a, b, c, d, x[0], 6, 0xF4292244);
+ II(ref d, a, b, c, x[7], 10, 0x432AFF97);
+ II(ref c, d, a, b, x[14], 15, 0xAB9423A7);
+ II(ref b, c, d, a, x[5], 21, 0xFC93A039);
+ II(ref a, b, c, d, x[12], 6, 0x655B59C3);
+ II(ref d, a, b, c, x[3], 10, 0x8F0CCC92);
+ II(ref c, d, a, b, x[10], 15, 0xFFEFF47D);
+ II(ref b, c, d, a, x[1], 21, 0x85845DD1);
+ II(ref a, b, c, d, x[8], 6, 0x6FA87E4F);
+ II(ref d, a, b, c, x[15], 10, 0xFE2CE6E0);
+ II(ref c, d, a, b, x[6], 15, 0xA3014314);
+ II(ref b, c, d, a, x[13], 21, 0x4E0811A1);
+ II(ref a, b, c, d, x[4], 6, 0xF7537E82);
+ II(ref d, a, b, c, x[11], 10, 0xBD3AF235);
+ II(ref c, d, a, b, x[2], 15, 0x2AD7D2BB);
+ II(ref b, c, d, a, x[9], 21, 0xEB86D391);
+
+ state[0] += a;
+ state[1] += b;
+ state[2] += c;
+ state[3] += d;
+ }
+
+ ///
+ /// Applies a single operation from the first MD5 round.
+ ///
+ /// The accumulator being updated.
+ /// The second state word.
+ /// The third state word.
+ /// The fourth state word.
+ /// The word of the block being mixed in.
+ /// The amount to rotate the accumulator by.
+ /// The constant for this operation.
+ private static void FF(ref uint a, uint b, uint c, uint d, uint x, int s, uint ac)
+ {
+ a += ((b & c) | (~b & d)) + x + ac;
+ a = RotateLeft(a, s) + b;
+ }
+
+ ///
+ private static void GG(ref uint a, uint b, uint c, uint d, uint x, int s, uint ac)
+ {
+ a += ((b & d) | (c & ~d)) + x + ac;
+ a = RotateLeft(a, s) + b;
+ }
+
+ ///
+ private static void HH(ref uint a, uint b, uint c, uint d, uint x, int s, uint ac)
+ {
+ a += (b ^ c ^ d) + x + ac;
+ a = RotateLeft(a, s) + b;
+ }
+
+ ///
+ private static void II(ref uint a, uint b, uint c, uint d, uint x, int s, uint ac)
+ {
+ a += (c ^ (b | ~d)) + x + ac;
+ a = RotateLeft(a, s) + b;
+ }
+
+ ///
+ /// Rotates a value to the left by a given amount.
+ ///
+ /// The value to rotate.
+ /// The amount to rotate by.
+ /// The rotated value.
+ private static uint RotateLeft(uint value, int offset)
+ {
+ return (value << offset) | (value >> (32 - offset));
+ }
+}
diff --git a/src/ComputeSharp.D2D1/Shaders/Translation/Dxbc.cs b/src/ComputeSharp.D2D1/Shaders/Translation/Dxbc.cs
new file mode 100644
index 000000000..f659db5e7
--- /dev/null
+++ b/src/ComputeSharp.D2D1/Shaders/Translation/Dxbc.cs
@@ -0,0 +1,232 @@
+using System;
+using System.Buffers.Binary;
+
+namespace ComputeSharp.D2D1.Shaders.Translation;
+
+///
+/// A helper type to inspect and patch DXBC shader containers produced by FXC.
+///
+///
+/// A DXBC container is a header followed by an unordered sequence of blobs:
+///
+/// - A 4 bytes 'DXBC' signature.
+/// - A 16 bytes checksum of the rest of the container.
+/// - A 4 bytes version, a 4 bytes total container size, and a 4 bytes blob count.
+/// - One 4 bytes offset per blob, relative to the start of the container.
+/// - Each blob, made of a 4 bytes signature, a 4 bytes payload size, and the payload.
+///
+/// All values are stored in little endian order.
+///
+internal static partial class Dxbc
+{
+ ///
+ /// The 'DXBC' signature at the start of every DXBC container.
+ ///
+ private const uint ContainerSignature = 0x43425844;
+
+ ///
+ /// The 'SFI0' signature of the shader feature info blob.
+ ///
+ private const uint ShaderFeatureInfoSignature = 0x30494653;
+
+ ///
+ /// The shader feature flag indicating that a shader declares minimum precision support.
+ ///
+ ///
+ /// This matches D3D_SHADER_FEATURE_MINIMUM_PRECISION from d3dcommon.h.
+ ///
+ private const ulong MinimumPrecisionShaderFeatureFlag = 0x0010;
+
+ ///
+ /// The offset of the container signature.
+ ///
+ private const int SignatureOffset = 0;
+
+ ///
+ /// The offset of the total container size.
+ ///
+ private const int ContainerSizeOffset = 24;
+
+ ///
+ /// The offset of the blob count.
+ ///
+ private const int BlobCountOffset = 28;
+
+ ///
+ /// The offset of the table of blob offsets, which is also the size of the fixed header.
+ ///
+ private const int BlobOffsetsOffset = 32;
+
+ ///
+ /// The size of the header of each blob (a signature and a payload size).
+ ///
+ private const int BlobHeaderSize = 8;
+
+ ///
+ /// The size of the payload of a shader feature info blob (a single 64 bit set of flags).
+ ///
+ private const int ShaderFeatureInfoPayloadSize = 8;
+
+ ///
+ /// Creates a copy of a DXBC container that declares minimum precision support.
+ ///
+ /// The DXBC container to copy and patch.
+ /// A copy of declaring minimum precision support.
+ /// Thrown if is not a well formed DXBC container.
+ ///
+ /// If the input container already has a shader feature info blob, the minimum precision flag is set on it.
+ /// Otherwise, a new shader feature info blob declaring just that flag is appended to the container.
+ ///
+ public static byte[] CreateWithMinimumPrecisionShaderFeatureFlag(ReadOnlySpan bytecode)
+ {
+ int blobCount = ValidateAndGetBlobCount(bytecode);
+ byte[] patchedBytecode;
+
+ // If the container already declares its shader features, just set the flag in place
+ if (TryGetShaderFeatureInfoPayloadOffset(bytecode, blobCount, out int payloadOffset))
+ {
+ patchedBytecode = bytecode.ToArray();
+
+ ulong featureFlags = BinaryPrimitives.ReadUInt64LittleEndian(patchedBytecode.AsSpan(payloadOffset));
+
+ BinaryPrimitives.WriteUInt64LittleEndian(
+ patchedBytecode.AsSpan(payloadOffset),
+ featureFlags | MinimumPrecisionShaderFeatureFlag);
+ }
+ else
+ {
+ patchedBytecode = CreateWithShaderFeatureInfoBlob(bytecode, blobCount);
+ }
+
+ // The contents of the container changed, so its checksum has to be recomputed. FXC APIs
+ // such as D3DSetBlobPart validate the checksum of their input and reject it otherwise.
+ UpdateChecksum(patchedBytecode);
+
+ return patchedBytecode;
+ }
+
+ ///
+ /// Creates a copy of a DXBC container with an additional shader feature info blob appended to it.
+ ///
+ /// The DXBC container to copy and patch.
+ /// The number of blobs in .
+ /// A copy of with a shader feature info blob.
+ private static byte[] CreateWithShaderFeatureInfoBlob(ReadOnlySpan bytecode, int blobCount)
+ {
+ // Appending a blob also adds one entry to the table of blob offsets, which shifts the
+ // body of the container (ie. all existing blobs) forward by the size of that entry.
+ const int BlobOffsetSize = sizeof(uint);
+ const int AppendedBlobSize = BlobHeaderSize + ShaderFeatureInfoPayloadSize;
+
+ int bodyOffset = BlobOffsetsOffset + (blobCount * BlobOffsetSize);
+ int patchedBodyOffset = bodyOffset + BlobOffsetSize;
+ int patchedBlobOffset = bytecode.Length + BlobOffsetSize;
+
+ byte[] patchedBytecode = new byte[bytecode.Length + BlobOffsetSize + AppendedBlobSize];
+
+ // Copy the fixed header, then the existing body after the enlarged table of blob offsets
+ bytecode.Slice(0, BlobOffsetsOffset).CopyTo(patchedBytecode);
+ bytecode.Slice(bodyOffset).CopyTo(patchedBytecode.AsSpan(patchedBodyOffset));
+
+ BinaryPrimitives.WriteUInt32LittleEndian(patchedBytecode.AsSpan(ContainerSizeOffset), (uint)patchedBytecode.Length);
+ BinaryPrimitives.WriteUInt32LittleEndian(patchedBytecode.AsSpan(BlobCountOffset), (uint)(blobCount + 1));
+
+ // Shift the existing blob offsets to account for the new entry in the table
+ for (int i = 0; i < blobCount; i++)
+ {
+ int blobOffsetOffset = BlobOffsetsOffset + (i * BlobOffsetSize);
+ uint blobOffset = BinaryPrimitives.ReadUInt32LittleEndian(bytecode.Slice(blobOffsetOffset));
+
+ BinaryPrimitives.WriteUInt32LittleEndian(patchedBytecode.AsSpan(blobOffsetOffset), blobOffset + BlobOffsetSize);
+ }
+
+ // Add the entry for the appended blob, and then the blob itself
+ BinaryPrimitives.WriteUInt32LittleEndian(patchedBytecode.AsSpan(bodyOffset), (uint)patchedBlobOffset);
+ BinaryPrimitives.WriteUInt32LittleEndian(patchedBytecode.AsSpan(patchedBlobOffset), ShaderFeatureInfoSignature);
+ BinaryPrimitives.WriteUInt32LittleEndian(patchedBytecode.AsSpan(patchedBlobOffset + BlobOffsetSize), ShaderFeatureInfoPayloadSize);
+ BinaryPrimitives.WriteUInt64LittleEndian(patchedBytecode.AsSpan(patchedBlobOffset + BlobHeaderSize), MinimumPrecisionShaderFeatureFlag);
+
+ return patchedBytecode;
+ }
+
+ ///
+ /// Validates that a given buffer is a well formed DXBC container, and gets the number of blobs in it.
+ ///
+ /// The DXBC container to validate.
+ /// The number of blobs in .
+ /// Thrown if is not a well formed DXBC container.
+ private static int ValidateAndGetBlobCount(ReadOnlySpan bytecode)
+ {
+ if (bytecode.Length < BlobOffsetsOffset ||
+ BinaryPrimitives.ReadUInt32LittleEndian(bytecode.Slice(SignatureOffset)) != ContainerSignature ||
+ BinaryPrimitives.ReadUInt32LittleEndian(bytecode.Slice(ContainerSizeOffset)) != (uint)bytecode.Length)
+ {
+ return ThrowArgumentExceptionForInvalidContainer();
+ }
+
+ uint blobCount = BinaryPrimitives.ReadUInt32LittleEndian(bytecode.Slice(BlobCountOffset));
+
+ // Ensure the table of blob offsets is in bounds before walking it
+ if (BlobOffsetsOffset + ((long)blobCount * sizeof(uint)) > bytecode.Length)
+ {
+ return ThrowArgumentExceptionForInvalidContainer();
+ }
+
+ for (int i = 0; i < blobCount; i++)
+ {
+ uint blobOffset = BinaryPrimitives.ReadUInt32LittleEndian(bytecode.Slice(BlobOffsetsOffset + (i * sizeof(uint))));
+
+ // Ensure the header of the blob is in bounds before reading the size of its payload from it
+ if (blobOffset + (long)BlobHeaderSize > bytecode.Length)
+ {
+ return ThrowArgumentExceptionForInvalidContainer();
+ }
+
+ uint blobSize = BinaryPrimitives.ReadUInt32LittleEndian(bytecode.Slice((int)blobOffset + sizeof(uint)));
+
+ if (blobOffset + (long)BlobHeaderSize + blobSize > bytecode.Length)
+ {
+ return ThrowArgumentExceptionForInvalidContainer();
+ }
+ }
+
+ return (int)blobCount;
+ }
+
+ ///
+ /// Tries to get the offset of the payload of the shader feature info blob in a DXBC container.
+ ///
+ /// The DXBC container to inspect.
+ /// The number of blobs in .
+ /// The resulting offset of the shader feature info payload, if found.
+ /// Whether the shader feature info blob was found.
+ private static bool TryGetShaderFeatureInfoPayloadOffset(ReadOnlySpan bytecode, int blobCount, out int payloadOffset)
+ {
+ for (int i = 0; i < blobCount; i++)
+ {
+ int blobOffset = (int)BinaryPrimitives.ReadUInt32LittleEndian(bytecode.Slice(BlobOffsetsOffset + (i * sizeof(uint))));
+
+ // Only consider the blob if it can actually hold a full set of shader feature flags
+ if (BinaryPrimitives.ReadUInt32LittleEndian(bytecode.Slice(blobOffset)) == ShaderFeatureInfoSignature &&
+ BinaryPrimitives.ReadUInt32LittleEndian(bytecode.Slice(blobOffset + sizeof(uint))) >= ShaderFeatureInfoPayloadSize)
+ {
+ payloadOffset = blobOffset + BlobHeaderSize;
+
+ return true;
+ }
+ }
+
+ payloadOffset = 0;
+
+ return false;
+ }
+
+ ///
+ /// Throws an for a malformed DXBC container.
+ ///
+ /// This method always throws and never actually returns.
+ private static int ThrowArgumentExceptionForInvalidContainer()
+ {
+ throw new ArgumentException("The input bytecode is not a well formed DXBC container.", "bytecode");
+ }
+}
diff --git a/tests/ComputeSharp.D2D1.Tests/D2D1PixelShaderTests.cs b/tests/ComputeSharp.D2D1.Tests/D2D1PixelShaderTests.cs
index aa085cc86..0ec2253fc 100644
--- a/tests/ComputeSharp.D2D1.Tests/D2D1PixelShaderTests.cs
+++ b/tests/ComputeSharp.D2D1.Tests/D2D1PixelShaderTests.cs
@@ -9,6 +9,9 @@
#pragma warning disable IDE0044, IDE0059, IDE0161
+// Some tests are specifically validating the experimental DeclareMinimumPrecisionSupport option
+#pragma warning disable CMPSEXP0001
+
[D2DInputCount(0)]
[D2DGeneratedPixelShaderDescriptor]
[AutoConstructor]
@@ -710,6 +713,35 @@ public float4 Execute()
}
}
+ [TestMethod]
+ public void LoadBytecode_DeclareMinimumPrecisionSupportIsAppliedCorrectly()
+ {
+ ReadOnlyMemory hlslBytecode1 = D2D1PixelShader.LoadBytecode(out _, out D2D1CompileOptions compileOptions1);
+ ReadOnlyMemory hlslBytecode2 = D2D1PixelShader.LoadBytecode(out _, out D2D1CompileOptions compileOptions2);
+
+ Assert.AreEqual(D2D1CompileOptions.Default, compileOptions1);
+ Assert.AreEqual(D2D1CompileOptions.Default | D2D1CompileOptions.DeclareMinimumPrecisionSupport, compileOptions2);
+
+ // Same check as in D2D1ShaderCompilerTests.CompileInvertEffectWithDeclareMinimumPrecisionSupport
+ Assert.AreEqual(hlslBytecode1.Length + 20, hlslBytecode2.Length);
+ }
+
+ [D2DInputCount(1)]
+ [D2DInputSimple(0)]
+ [D2DShaderProfile(D2D1ShaderProfile.PixelShader50)]
+ [D2DCompileOptions(D2D1CompileOptions.Default | D2D1CompileOptions.DeclareMinimumPrecisionSupport)]
+ [D2DGeneratedPixelShaderDescriptor]
+ public readonly partial struct ReferenceShaderWithDeclareMinimumPrecisionSupport : ID2D1PixelShader
+ {
+ public float4 Execute()
+ {
+ float4 color = D2D.GetInput(0);
+ float3 rgb = Hlsl.Saturate(1.0f - color.RGB);
+
+ return new(rgb, 1);
+ }
+ }
+
[TestMethod]
public void GetConstantBufferSize_Empty()
{
diff --git a/tests/ComputeSharp.D2D1.Tests/D2D1ShaderCompilerTests.cs b/tests/ComputeSharp.D2D1.Tests/D2D1ShaderCompilerTests.cs
index 577ca088b..fc79d27f7 100644
--- a/tests/ComputeSharp.D2D1.Tests/D2D1ShaderCompilerTests.cs
+++ b/tests/ComputeSharp.D2D1.Tests/D2D1ShaderCompilerTests.cs
@@ -1,7 +1,11 @@
using System;
+using System.Buffers.Binary;
using ComputeSharp.D2D1.Interop;
using Microsoft.VisualStudio.TestTools.UnitTesting;
+// These tests are specifically validating the experimental DeclareMinimumPrecisionSupport option
+#pragma warning disable CMPSEXP0001
+
namespace ComputeSharp.D2D1.Tests;
[TestClass]
@@ -315,4 +319,97 @@ public void CompileShaderWithWarning_Suppressed()
Assert.IsTrue(bytecode.Length > 0);
}
+
+ [TestMethod]
+ public void CompileInvertEffectWithDeclareMinimumPrecisionSupport()
+ {
+ ReadOnlyMemory bytecode = D2D1ShaderCompiler.Compile(
+ InvertEffectSource.AsSpan(),
+ "PSMain".AsSpan(),
+ D2D1ShaderProfile.PixelShader40Level93,
+ D2D1CompileOptions.Default);
+
+ ReadOnlyMemory bytecodeWithRetention = D2D1ShaderCompiler.Compile(
+ InvertEffectSource.AsSpan(),
+ "PSMain".AsSpan(),
+ D2D1ShaderProfile.PixelShader40Level93,
+ D2D1CompileOptions.Default | D2D1CompileOptions.DeclareMinimumPrecisionSupport);
+
+ // The only difference is the appended shader feature info blob: one entry in the table of
+ // blob offsets (4 bytes), the header of the blob (8 bytes), and its payload (8 bytes).
+ Assert.AreEqual(bytecode.Length + 20, bytecodeWithRetention.Length);
+
+ // Compiling succeeds only if D3DSetBlobPart accepted the patched container, and the resulting
+ // bytecode is only usable if the checksum was recomputed over the patched contents.
+ Assert.IsTrue(IsWellFormedDxbcContainer(bytecodeWithRetention.Span));
+ }
+
+ [TestMethod]
+ public void CompileInvertEffectWithDeclareMinimumPrecisionSupportAndNoLinking()
+ {
+ ReadOnlyMemory bytecode = D2D1ShaderCompiler.Compile(
+ InvertEffectSource.AsSpan(),
+ "PSMain".AsSpan(),
+ D2D1ShaderProfile.PixelShader40Level93,
+ D2D1CompileOptions.Default & ~D2D1CompileOptions.EnableLinking);
+
+ ReadOnlyMemory bytecodeWithRetention = D2D1ShaderCompiler.Compile(
+ InvertEffectSource.AsSpan(),
+ "PSMain".AsSpan(),
+ D2D1ShaderProfile.PixelShader40Level93,
+ (D2D1CompileOptions.Default & ~D2D1CompileOptions.EnableLinking) | D2D1CompileOptions.DeclareMinimumPrecisionSupport);
+
+ // There is no export function to reach without linking, so the option is ignored
+ CollectionAssert.AreEqual(bytecode.ToArray(), bytecodeWithRetention.ToArray());
+ }
+
+ ///
+ /// The HLSL source for a simple invert effect, shared by tests comparing compilation options.
+ ///
+ private const string InvertEffectSource = """
+ #define D2D_INPUT_COUNT 1
+ #define D2D_INPUT0_SIMPLE
+
+ #include "d2d1effecthelpers.hlsli"
+
+ D2D_PS_ENTRY(PSMain)
+ {
+ float4 color = D2DGetInput(0);
+ float3 rgb = saturate(1.0 - color.rgb);
+ return float4(rgb, 1);
+ }
+ """;
+
+ ///
+ /// Checks that a buffer is a DXBC container with a consistent size and set of blob offsets.
+ ///
+ /// The DXBC container to inspect.
+ /// Whether is a well formed DXBC container.
+ private static bool IsWellFormedDxbcContainer(ReadOnlySpan bytecode)
+ {
+ if (bytecode.Length < 32 || !bytecode.StartsWith("DXBC"u8))
+ {
+ return false;
+ }
+
+ if (BinaryPrimitives.ReadUInt32LittleEndian(bytecode.Slice(24)) != (uint)bytecode.Length)
+ {
+ return false;
+ }
+
+ uint blobCount = BinaryPrimitives.ReadUInt32LittleEndian(bytecode.Slice(28));
+
+ for (int i = 0; i < blobCount; i++)
+ {
+ uint blobOffset = BinaryPrimitives.ReadUInt32LittleEndian(bytecode.Slice(32 + (i * 4)));
+ uint blobSize = BinaryPrimitives.ReadUInt32LittleEndian(bytecode.Slice((int)blobOffset + 4));
+
+ if (blobOffset + 8 + blobSize > bytecode.Length)
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
}
\ No newline at end of file