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