From 625ee049387727b3bf9257be1ade83ad0e909808 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 30 Jul 2026 14:29:27 -0700 Subject: [PATCH 1/3] Add 'Dxbc' helpers to patch shader feature flags Adds support for appending (or updating) the 'SFI0' shader feature info blob in a DXBC container, along with the modified MD5 checksum used by DXBC containers. Recomputing the checksum is required, as FXC APIs such as 'D3DSetBlobPart' validate it and reject any container that was modified after being compiled. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: a62113de-a3b2-457b-abce-70ee666c9e6f --- .../ComputeSharp.D2D1.SourceGenerators.csproj | 2 + .../Shaders/Translation/Dxbc.Checksum.cs | 275 ++++++++++++++++++ .../Shaders/Translation/Dxbc.cs | 232 +++++++++++++++ 3 files changed, 509 insertions(+) create mode 100644 src/ComputeSharp.D2D1/Shaders/Translation/Dxbc.Checksum.cs create mode 100644 src/ComputeSharp.D2D1/Shaders/Translation/Dxbc.cs 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/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"); + } +} From 95310557f485d9b673af8051779eba53acdb9409 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 30 Jul 2026 15:04:01 -0700 Subject: [PATCH 2/3] Add experimental 'DeclareMinimumPrecisionSupport' compile option Setting 'EnableLinking' 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. This option 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. This relies on behavior that is neither documented nor guaranteed, so the option is marked as experimental. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: a62113de-a3b2-457b-abce-70ee666c9e6f --- .../Attributes/Enums/D2D1CompileOptions.cs | 29 ++++++++++ .../Shaders/Interop/D2D1ShaderCompiler.cs | 9 ++- .../Shaders/Translation/D3DCompiler.cs | 58 ++++++++++++++----- 3 files changed, 80 insertions(+), 16 deletions(-) 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(); } From c058cb6c2bb085dcfd01cb419c84b89a2668b13f Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 30 Jul 2026 15:04:01 -0700 Subject: [PATCH 3/3] Add tests for 'DeclareMinimumPrecisionSupport' Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: a62113de-a3b2-457b-abce-70ee666c9e6f --- .../D2D1PixelShaderTests.cs | 32 ++++++ .../D2D1ShaderCompilerTests.cs | 97 +++++++++++++++++++ 2 files changed, 129 insertions(+) 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