diff --git a/src/ComputeSharp.D2D1.SourceGenerators/D2DPixelShaderDescriptorGenerator.cs b/src/ComputeSharp.D2D1.SourceGenerators/D2DPixelShaderDescriptorGenerator.cs
index 1d1459093..0ebdc716e 100644
--- a/src/ComputeSharp.D2D1.SourceGenerators/D2DPixelShaderDescriptorGenerator.cs
+++ b/src/ComputeSharp.D2D1.SourceGenerators/D2DPixelShaderDescriptorGenerator.cs
@@ -26,8 +26,11 @@ public sealed partial class D2DPixelShaderDescriptorGenerator : IIncrementalGene
///
public void Initialize(IncrementalGeneratorInitializationContext context)
{
- // Discover all shader types and extract all the necessary info from each of them
- IncrementalValuesProvider shaderInfo =
+ // Discover all shader types and extract all the necessary info from each of them (with the exception
+ // of the compiled HLSL bytecode, which is processed later). The info needed to synthesize the diagnostics
+ // for the deferred compilation is returned alongside each model (as it holds syntax tree references, it
+ // is intentionally not part of the model itself, so it never affects the equality of the shader models).
+ IncrementalValuesProvider<(D2D1ShaderInfo Info, HlslBytecodeDiagnosticsInfo DiagnosticsInfo)> shaderInfoWithDiagnosticsInfo =
context.SyntaxProvider
.ForAttributeWithMetadataName(
"ComputeSharp.D2D1.D2DGeneratedPixelShaderDescriptorAttribute",
@@ -152,22 +155,19 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
token.ThrowIfCancellationRequested();
- // As the last steps in the pipeline, try to compile the shader if needed.
- // This is done last so that it can be skipped if any errors happened before.
+ // Prepare the key to compile the shader afterwards. The compilation is deliberately not
+ // done here: the incremental driver invokes transform callbacks sequentially, so compiling
+ // here would serialize all shader compilations. Instead, compilation is deferred to a
+ // dedicated node below, which can process all shaders in the compilation in parallel.
HlslBytecodeInfoKey hlslInfoKey = new(
hlslSource,
effectiveShaderProfile,
effectiveCompileOptions,
isCompilationEnabled);
- // Get the existing compiled shader, or compile the processed HLSL code
- HlslBytecodeInfo hlslInfo = HlslBytecodeSyntaxProcessor.GetInfo(ref hlslInfoKey, token);
-
- token.ThrowIfCancellationRequested();
-
- // Append any diagnostic for the shader compilation
- HlslBytecodeSyntaxProcessor.GetInfoDiagnostics(typeSymbol, hlslInfo, diagnostics);
- HlslBytecodeSyntaxProcessor.GetDoublePrecisionSupportDiagnostics(typeSymbol, hlslInfo, diagnostics);
+ // Capture the info needed to synthesize the diagnostics for the deferred compilation,
+ // as they cannot be created later (symbols must not be used past the transform node)
+ HlslBytecodeDiagnosticsInfo hlslDiagnosticsInfo = HlslBytecodeSyntaxProcessor.GetDiagnosticsInfo(typeSymbol);
token.ThrowIfCancellationRequested();
@@ -176,7 +176,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
token.ThrowIfCancellationRequested();
- return new D2D1ShaderInfo(
+ D2D1ShaderInfo info = new(
Hierarchy: hierarchyInfo,
EffectId: effectId,
EffectDisplayName: effectDisplayName,
@@ -192,11 +192,62 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
ChannelDepth: channelDepth,
PixelOptions: pixelOptions,
HlslInfoKey: hlslInfoKey,
- HlslInfo: hlslInfo,
+ HlslInfo: HlslBytecodeInfo.Missing.Instance,
Diagnostcs: diagnostics.ToImmutable());
+
+ return (Info: info, DiagnosticsInfo: hlslDiagnosticsInfo);
})
- .WithTrackingName(WellKnownTrackingNames.Execute)
- .Where(static item => item is not null)!;
+ .Where(static item => item.Info is not null);
+
+ // Compile all shaders in parallel in a single dedicated node, warming up the shared bytecode
+ // cache. The node produces no meaningful value: it only exists so that the join node below has
+ // an edge ordering it after all compilations are done (its input requires this node's output).
+ IncrementalValueProvider hlslBytecodeCache =
+ shaderInfoWithDiagnosticsInfo
+ .Select(static (item, _) => item.Info.HlslInfoKey)
+ .Collect()
+ .Select(static (keys, token) =>
+ {
+ HlslBytecodeSyntaxProcessor.CompileAllInParallel(keys, token);
+
+ return true;
+ });
+
+ // Join each shader with its compiled bytecode (guaranteed to be a cache hit, given the ordering edge on
+ // the node above), and synthesize the deferred diagnostics for the shader compilation. The resulting
+ // models are the same that used to be produced directly by the transform node (hence the tracking name).
+ IncrementalValuesProvider shaderInfo =
+ shaderInfoWithDiagnosticsInfo
+ .Combine(hlslBytecodeCache)
+ .Select(static (pair, token) =>
+ {
+ (D2D1ShaderInfo info, HlslBytecodeDiagnosticsInfo diagnosticsInfo) = pair.Left;
+
+ HlslBytecodeInfoKey hlslInfoKey = info.HlslInfoKey;
+
+ // Get the compiled shader from the warmed up cache
+ HlslBytecodeInfo hlslInfo = HlslBytecodeSyntaxProcessor.GetInfo(ref hlslInfoKey, token);
+
+ token.ThrowIfCancellationRequested();
+
+ using ImmutableArrayBuilder diagnostics = new();
+
+ diagnostics.AddRange(info.Diagnostcs.AsSpan());
+
+ // Append any diagnostic for the shader compilation
+ HlslBytecodeSyntaxProcessor.GetInfoDiagnostics(diagnosticsInfo, hlslInfo, diagnostics);
+ HlslBytecodeSyntaxProcessor.GetDoublePrecisionSupportDiagnostics(diagnosticsInfo, hlslInfo, diagnostics);
+
+ token.ThrowIfCancellationRequested();
+
+ return info with
+ {
+ HlslInfoKey = hlslInfoKey,
+ HlslInfo = hlslInfo,
+ Diagnostcs = diagnostics.ToImmutable()
+ };
+ })
+ .WithTrackingName(WellKnownTrackingNames.Execute);
// We need to create two more incremental steps to ensure we correctly emit diagnostics and re-generate sources.
// First, select an incremental provider with just the diagnostics, which will trigger every time any of them changes.
diff --git a/src/ComputeSharp.SourceGeneration.Hlsl/ComputeSharp.SourceGeneration.Hlsl.projitems b/src/ComputeSharp.SourceGeneration.Hlsl/ComputeSharp.SourceGeneration.Hlsl.projitems
index abd0cb562..e71d4eab6 100644
--- a/src/ComputeSharp.SourceGeneration.Hlsl/ComputeSharp.SourceGeneration.Hlsl.projitems
+++ b/src/ComputeSharp.SourceGeneration.Hlsl/ComputeSharp.SourceGeneration.Hlsl.projitems
@@ -26,6 +26,7 @@
+
diff --git a/src/ComputeSharp.SourceGeneration.Hlsl/Models/HlslBytecodeDiagnosticsInfo.cs b/src/ComputeSharp.SourceGeneration.Hlsl/Models/HlslBytecodeDiagnosticsInfo.cs
new file mode 100644
index 000000000..6d8b4d827
--- /dev/null
+++ b/src/ComputeSharp.SourceGeneration.Hlsl/Models/HlslBytecodeDiagnosticsInfo.cs
@@ -0,0 +1,20 @@
+using Microsoft.CodeAnalysis;
+
+namespace ComputeSharp.SourceGeneration.Models;
+
+///
+/// A model capturing the info needed to synthesize diagnostics for compiled HLSL bytecode. This makes it possible
+/// to create such diagnostics after the transform node has completed (symbols cannot be used past that point), which
+/// in turn allows deferring the bytecode compilation (so it can be parallelized). Note that this model holds source
+/// locations (which reference syntax trees), so it is deliberately kept separate from the shader models: it only
+/// travels alongside them up to the node that creates the diagnostics, and it is dropped from that point onwards.
+///
+/// The fully qualified name of the shader type.
+/// The location of the shader type, if available.
+/// Whether the shader type is annotated to require double precision support.
+/// The location of the attribute requiring double precision support, if available.
+internal sealed record HlslBytecodeDiagnosticsInfo(
+ string TypeName,
+ Location? TypeLocation,
+ bool HasRequiresDoublePrecisionSupportAttribute,
+ Location? RequiresDoublePrecisionSupportAttributeLocation);
diff --git a/src/ComputeSharp.SourceGeneration.Hlsl/SyntaxProcessors/HlslBytecodeSyntaxProcessor.cs b/src/ComputeSharp.SourceGeneration.Hlsl/SyntaxProcessors/HlslBytecodeSyntaxProcessor.cs
index f530496ee..91c2d175e 100644
--- a/src/ComputeSharp.SourceGeneration.Hlsl/SyntaxProcessors/HlslBytecodeSyntaxProcessor.cs
+++ b/src/ComputeSharp.SourceGeneration.Hlsl/SyntaxProcessors/HlslBytecodeSyntaxProcessor.cs
@@ -1,8 +1,11 @@
using System;
using System.Collections.Immutable;
using System.ComponentModel;
+using System.Linq;
using System.Runtime.CompilerServices;
+using System.Runtime.ExceptionServices;
using System.Threading;
+using System.Threading.Tasks;
using ComputeSharp.SourceGeneration.Extensions;
using ComputeSharp.SourceGeneration.Helpers;
using ComputeSharp.SourceGeneration.Models;
@@ -89,13 +92,89 @@ static unsafe HlslBytecodeInfo GetInfo(HlslBytecodeInfoKey key, CancellationToke
}
///
- /// Gets any diagnostics from a processed instance.
+ /// Compiles the shaders for all input keys in parallel, warming up the shared cache.
+ /// After this call, calls for any of the input keys will be cache hits.
+ ///
+ /// The instances for the shaders to compile.
+ /// The used to cancel the operation, if needed.
+ public static void CompileAllInParallel(ImmutableArray keys, CancellationToken token)
+ {
+ static void Compile(HlslBytecodeInfoKey key, CancellationToken token)
+ {
+ _ = GetInfo(ref key, token);
+ }
+
+ // Skip the parallel dispatch entirely if there are less than two keys to process
+ if (keys.Length == 0)
+ {
+ return;
+ }
+
+ if (keys.Length == 1)
+ {
+ Compile(keys[0], token);
+
+ return;
+ }
+
+ try
+ {
+ // Compile all shaders in parallel (each compilation is independent, and both the shared cache and
+ // the native compilers support concurrent use). Duplicate keys are filtered out first: concurrent
+ // requests for the same key would be benign (one result would just be discarded), but there is no
+ // reason to schedule them at all. The order of compilations does not matter, as the results are
+ // only published to the cache here (callers will then retrieve them via cache hits afterwards).
+ _ = Parallel.ForEach(
+ keys.Distinct(),
+ new ParallelOptions { CancellationToken = token },
+ key => Compile(key, token));
+ }
+ catch (AggregateException e)
+ {
+ // If cancellation is requested, normalize to an OperationCanceledException for the incremental
+ // driver (a cancellation from the callbacks may be wrapped, depending on interleaving). Other
+ // exceptions cannot really occur, as the compilation callback catches all expected exceptions.
+ token.ThrowIfCancellationRequested();
+
+ // If there is a single unexpected exception, unwrap it and rethrow it (preserving its stack
+ // trace), so that the failure reported by the compiler names the actual exception type.
+ if (e.InnerExceptions.Count == 1)
+ {
+ ExceptionDispatchInfo.Capture(e.InnerExceptions[0]).Throw();
+ }
+
+ throw;
+ }
+ }
+
+ ///
+ /// Gets the instance for a given shader type.
+ /// This captures all info needed to synthesize compile diagnostics after the transform
+ /// node has completed (which is required, as symbols cannot be used past that point).
///
/// The input instance to process.
+ /// The instance for the current shader.
+ public static HlslBytecodeDiagnosticsInfo GetDiagnosticsInfo(INamedTypeSymbol structDeclarationSymbol)
+ {
+ bool hasRequiresDoublePrecisionSupportAttribute = structDeclarationSymbol.TryGetAttributeWithFullyQualifiedMetadataName(
+ GetRequiresDoublePrecisionSupportAttributeName(),
+ out AttributeData? attributeData);
+
+ return new HlslBytecodeDiagnosticsInfo(
+ TypeName: structDeclarationSymbol.ToDisplayString(),
+ TypeLocation: structDeclarationSymbol.Locations.FirstOrDefault(),
+ HasRequiresDoublePrecisionSupportAttribute: hasRequiresDoublePrecisionSupportAttribute,
+ RequiresDoublePrecisionSupportAttributeLocation: attributeData?.GetLocation());
+ }
+
+ ///
+ /// Gets any diagnostics from a processed instance.
+ ///
+ /// The instance for the current shader.
/// The source instance.
/// The collection of produced instances.
public static void GetInfoDiagnostics(
- INamedTypeSymbol structDeclarationSymbol,
+ HlslBytecodeDiagnosticsInfo diagnosticsInfo,
HlslBytecodeInfo info,
ImmutableArrayBuilder diagnostics)
{
@@ -105,8 +184,8 @@ public static void GetInfoDiagnostics(
{
diagnostic = DiagnosticInfo.Create(
HlslBytecodeFailedWithWin32Exception,
- structDeclarationSymbol,
- structDeclarationSymbol,
+ diagnosticsInfo.TypeLocation,
+ diagnosticsInfo.TypeName,
win32Error.HResult,
win32Error.Message);
}
@@ -114,8 +193,8 @@ public static void GetInfoDiagnostics(
{
diagnostic = DiagnosticInfo.Create(
HlslBytecodeFailedWithCompilationException,
- structDeclarationSymbol,
- structDeclarationSymbol,
+ diagnosticsInfo.TypeLocation,
+ diagnosticsInfo.TypeName,
fxcError.Message);
}
@@ -128,11 +207,11 @@ public static void GetInfoDiagnostics(
///
/// Gets the diagnostics for when double precision support is configured incorrectly.
///
- /// The input instance to process.
+ /// The instance for the current shader.
/// The source instance.
/// The collection of produced instances.
public static void GetDoublePrecisionSupportDiagnostics(
- INamedTypeSymbol structDeclarationSymbol,
+ HlslBytecodeDiagnosticsInfo diagnosticsInfo,
HlslBytecodeInfo info,
ImmutableArrayBuilder diagnostics)
{
@@ -142,26 +221,22 @@ public static void GetDoublePrecisionSupportDiagnostics(
return;
}
- bool hasRequiresDoublePrecisionSupportAttribute = structDeclarationSymbol.TryGetAttributeWithFullyQualifiedMetadataName(
- GetRequiresDoublePrecisionSupportAttributeName(),
- out AttributeData? attributeData);
-
// Check the two cases where diagnostics are necessary:
// - The shader does not have [[D2D]RequiresDoublePrecisionSupport], but it needs it
// - The shader has [[D2D]RequiresDoublePrecisionSupport], but it does not need it
- if (!hasRequiresDoublePrecisionSupportAttribute && success.RequiresDoublePrecisionSupport)
+ if (!diagnosticsInfo.HasRequiresDoublePrecisionSupportAttribute && success.RequiresDoublePrecisionSupport)
{
diagnostics.Add(DiagnosticInfo.Create(
MissingRequiresDoublePrecisionSupportAttribute,
- structDeclarationSymbol,
- structDeclarationSymbol));
+ diagnosticsInfo.TypeLocation,
+ diagnosticsInfo.TypeName));
}
- else if (hasRequiresDoublePrecisionSupportAttribute && !success.RequiresDoublePrecisionSupport)
+ else if (diagnosticsInfo.HasRequiresDoublePrecisionSupportAttribute && !success.RequiresDoublePrecisionSupport)
{
diagnostics.Add(DiagnosticInfo.Create(
UnnecessaryRequiresDoublePrecisionSupportAttribute,
- attributeData!.GetLocation(),
- structDeclarationSymbol));
+ diagnosticsInfo.RequiresDoublePrecisionSupportAttributeLocation,
+ diagnosticsInfo.TypeName));
}
}
diff --git a/src/ComputeSharp.SourceGenerators/ComputeShaderDescriptorGenerator.cs b/src/ComputeSharp.SourceGenerators/ComputeShaderDescriptorGenerator.cs
index 1ed999402..c614ac592 100644
--- a/src/ComputeSharp.SourceGenerators/ComputeShaderDescriptorGenerator.cs
+++ b/src/ComputeSharp.SourceGenerators/ComputeShaderDescriptorGenerator.cs
@@ -26,8 +26,9 @@ public sealed partial class ComputeShaderDescriptorGenerator : IIncrementalGener
///
public void Initialize(IncrementalGeneratorInitializationContext context)
{
- // Discover all shader types and extract all the necessary info from each of them
- IncrementalValuesProvider shaderInfo =
+ // Discover all shader types and extract all the necessary info from each of them (with the exception
+ // of the compiled HLSL bytecode, which is processed later). See more notes in the D2D1 generator.
+ IncrementalValuesProvider<(ShaderInfo Info, HlslBytecodeDiagnosticsInfo DiagnosticsInfo)> shaderInfoWithDiagnosticsInfo =
context.SyntaxProvider
.ForAttributeWithMetadataName(
"ComputeSharp.GeneratedComputeShaderDescriptorAttribute",
@@ -117,16 +118,15 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
token.ThrowIfCancellationRequested();
- // Prepare the lookup key for the HLSL shader bytecode
+ // Prepare the lookup key for the HLSL shader bytecode. The compilation is deliberately
+ // deferred to a dedicated node below, so all shaders can be compiled in parallel there
+ // (the incremental driver invokes transform callbacks sequentially, so compiling here
+ // would serialize all shader compilations).
HlslBytecodeInfoKey hlslInfoKey = new(hlslSource, compileOptions, isCompilationEnabled);
- // Try to get the HLSL bytecode
- HlslBytecodeInfo hlslInfo = HlslBytecodeSyntaxProcessor.GetInfo(ref hlslInfoKey, token);
-
- token.ThrowIfCancellationRequested();
-
- HlslBytecodeSyntaxProcessor.GetInfoDiagnostics(typeSymbol, hlslInfo, diagnostics);
- HlslBytecodeSyntaxProcessor.GetDoublePrecisionSupportDiagnostics(typeSymbol, hlslInfo, diagnostics);
+ // Capture the info needed to synthesize the diagnostics for the deferred compilation,
+ // as they cannot be created later (symbols must not be used past the transform node)
+ HlslBytecodeDiagnosticsInfo hlslDiagnosticsInfo = HlslBytecodeSyntaxProcessor.GetDiagnosticsInfo(typeSymbol);
token.ThrowIfCancellationRequested();
@@ -135,7 +135,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
token.ThrowIfCancellationRequested();
- return new ShaderInfo(
+ ShaderInfo info = new(
Hierarchy: hierarchyInfo,
ThreadsX: threadsX,
ThreadsY: threadsY,
@@ -147,11 +147,61 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
Resources: resourceInfo,
ResourceDescriptors: resourceDescriptors,
HlslInfoKey: hlslInfoKey,
- HlslInfo: hlslInfo,
+ HlslInfo: HlslBytecodeInfo.Missing.Instance,
Diagnostcs: diagnostics.ToImmutable());
+
+ return (Info: info, DiagnosticsInfo: hlslDiagnosticsInfo);
})
- .WithTrackingName(WellKnownTrackingNames.Execute)
- .Where(static item => item is not null)!;
+ .Where(static item => item.Info is not null);
+
+ // Compile all shaders in parallel in a single dedicated node, warming up the shared bytecode
+ // cache (see more notes in the D2D1 generator). The node produces no meaningful value: it only
+ // exists so that the join node below has an edge ordering it after all compilations are done.
+ IncrementalValueProvider hlslBytecodeCache =
+ shaderInfoWithDiagnosticsInfo
+ .Select(static (item, _) => item.Info.HlslInfoKey)
+ .Collect()
+ .Select(static (keys, token) =>
+ {
+ HlslBytecodeSyntaxProcessor.CompileAllInParallel(keys, token);
+
+ return true;
+ });
+
+ // Join each shader with its compiled bytecode (guaranteed to be a cache hit, given the ordering
+ // edge on the node above), and synthesize the deferred diagnostics for the shader compilation
+ IncrementalValuesProvider shaderInfo =
+ shaderInfoWithDiagnosticsInfo
+ .Combine(hlslBytecodeCache)
+ .Select(static (pair, token) =>
+ {
+ (ShaderInfo info, HlslBytecodeDiagnosticsInfo diagnosticsInfo) = pair.Left;
+
+ HlslBytecodeInfoKey hlslInfoKey = info.HlslInfoKey;
+
+ // Get the compiled shader from the warmed up cache
+ HlslBytecodeInfo hlslInfo = HlslBytecodeSyntaxProcessor.GetInfo(ref hlslInfoKey, token);
+
+ token.ThrowIfCancellationRequested();
+
+ using ImmutableArrayBuilder diagnostics = new();
+
+ diagnostics.AddRange(info.Diagnostcs.AsSpan());
+
+ // Append any diagnostic for the shader compilation
+ HlslBytecodeSyntaxProcessor.GetInfoDiagnostics(diagnosticsInfo, hlslInfo, diagnostics);
+ HlslBytecodeSyntaxProcessor.GetDoublePrecisionSupportDiagnostics(diagnosticsInfo, hlslInfo, diagnostics);
+
+ token.ThrowIfCancellationRequested();
+
+ return info with
+ {
+ HlslInfoKey = hlslInfoKey,
+ HlslInfo = hlslInfo,
+ Diagnostcs = diagnostics.ToImmutable()
+ };
+ })
+ .WithTrackingName(WellKnownTrackingNames.Execute);
// Split the diagnostics, and drop them from the output provider (see more notes in the D2D1 generator)
IncrementalValuesProvider> diagnosticInfo =
diff --git a/tests/ComputeSharp.D2D1.Tests.SourceGenerators/Test_D2DPixelShaderDescriptorGenerator_Diagnostics.cs b/tests/ComputeSharp.D2D1.Tests.SourceGenerators/Test_D2DPixelShaderDescriptorGenerator_Diagnostics.cs
index caf81758f..8a9c3ef90 100644
--- a/tests/ComputeSharp.D2D1.Tests.SourceGenerators/Test_D2DPixelShaderDescriptorGenerator_Diagnostics.cs
+++ b/tests/ComputeSharp.D2D1.Tests.SourceGenerators/Test_D2DPixelShaderDescriptorGenerator_Diagnostics.cs
@@ -31,7 +31,7 @@ public float4 Execute()
}
""";
- CSharpGeneratorTest.VerifyDiagnostics(source, "CMPSD2D0080");
+ CSharpGeneratorTest.VerifyDiagnostics(source, ("CMPSD2D0080", "MyShader"));
}
[TestMethod]
@@ -59,6 +59,49 @@ public float4 Execute()
}
""";
- CSharpGeneratorTest.VerifyDiagnostics(source, "CMPSD2D0081");
+ CSharpGeneratorTest.VerifyDiagnostics(source, ("CMPSD2D0081", "D2DRequiresDoublePrecisionSupport"));
+ }
+
+ [TestMethod]
+ public void HlslBytecodeFailedWithCompilationException()
+ {
+ const string source = """
+ using ComputeSharp;
+ using ComputeSharp.D2D1;
+ using float4 = global::ComputeSharp.Float4;
+
+ namespace MyNamespace;
+
+ internal static class ClassWithRecursiveMembers
+ {
+ public static int A(int x)
+ {
+ if (x <= 1)
+ {
+ return x;
+ }
+
+ return A(x - 1);
+ }
+ }
+
+ [D2DInputCount(0)]
+ [D2DShaderProfile(D2D1ShaderProfile.PixelShader50)]
+ [D2DGeneratedPixelShaderDescriptor]
+ internal readonly partial struct MyShader : ID2D1PixelShader
+ {
+ private readonly int value;
+
+ public float4 Execute()
+ {
+ return ClassWithRecursiveMembers.A(value);
+ }
+ }
+ """;
+
+ // The HLSL compilation is expected to fail because recursion is not supported. This also
+ // verifies that the diagnostic is reported on the shader type, even though the compilation
+ // is deferred to a separate incremental node (ie. it's no longer done in the transform node).
+ CSharpGeneratorTest.VerifyDiagnostics(source, ("CMPSD2D0034", "MyShader"));
}
}
\ No newline at end of file
diff --git a/tests/ComputeSharp.Tests.SourceGenerators/Helpers/CSharpGeneratorTest{TGenerator}.cs b/tests/ComputeSharp.Tests.SourceGenerators/Helpers/CSharpGeneratorTest{TGenerator}.cs
index 08432e47d..efb85189a 100644
--- a/tests/ComputeSharp.Tests.SourceGenerators/Helpers/CSharpGeneratorTest{TGenerator}.cs
+++ b/tests/ComputeSharp.Tests.SourceGenerators/Helpers/CSharpGeneratorTest{TGenerator}.cs
@@ -17,6 +17,7 @@
using Basic.Reference.Assemblies;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace ComputeSharp.Tests.SourceGenerators.Helpers;
@@ -52,6 +53,35 @@ public static void VerifyDiagnostics(string source, params string[] diagnosticsI
}
}
+ ///
+ /// Verifies the resulting diagnostics from a source generator, including their locations.
+ ///
+ /// The input source to process.
+ /// The expected diagnostics, as pairs of ids and the (unique) text in they should be reported on.
+ public static void VerifyDiagnostics(string source, params (string Id, string LocationText)[] diagnostics)
+ {
+ RunGenerator(source, out _, out ImmutableArray actualDiagnostics);
+
+ Dictionary diagnosticMap = actualDiagnostics.DistinctBy(diagnostic => diagnostic.Id).ToDictionary(diagnostic => diagnostic.Id);
+
+ // Check that the diagnostics match
+ Assert.IsTrue(diagnosticMap.Keys.ToHashSet().SetEquals(diagnostics.Select(diagnostic => diagnostic.Id)), $"Diagnostics didn't match. {string.Join(", ", diagnosticMap.Values)}");
+
+ // Check that each diagnostic has the expected source location
+ foreach ((string id, string locationText) in diagnostics)
+ {
+ int index = source.IndexOf(locationText, StringComparison.Ordinal);
+
+ Assert.IsTrue(index >= 0, $"The text '{locationText}' was not found in the source.");
+ Assert.IsTrue(source.IndexOf(locationText, index + 1, StringComparison.Ordinal) < 0, $"The text '{locationText}' must appear exactly once in the source.");
+
+ Location location = diagnosticMap[id].Location;
+
+ Assert.IsTrue(location.IsInSource, $"Diagnostic '{id}' has no source location (kind: {location.Kind}).");
+ Assert.AreEqual(new TextSpan(index, locationText.Length), location.SourceSpan, $"Diagnostic '{id}' has an unexpected location.");
+ }
+ }
+
///
/// Verifies the resulting sources produced by a source generator.
///
diff --git a/tests/ComputeSharp.Tests.SourceGenerators/Test_ComputeShaderDescriptorGenerator_Diagnostics.cs b/tests/ComputeSharp.Tests.SourceGenerators/Test_ComputeShaderDescriptorGenerator_Diagnostics.cs
index 2f4a468d0..27ab278c8 100644
--- a/tests/ComputeSharp.Tests.SourceGenerators/Test_ComputeShaderDescriptorGenerator_Diagnostics.cs
+++ b/tests/ComputeSharp.Tests.SourceGenerators/Test_ComputeShaderDescriptorGenerator_Diagnostics.cs
@@ -1498,7 +1498,7 @@ public void Execute()
// The HLSL compilation is expected to fail because recursion is not actually supported:
//
// "error: recursive functions not allowed"
- CSharpGeneratorTest.VerifyDiagnostics(source, "CMPS0046");
+ CSharpGeneratorTest.VerifyDiagnostics(source, ("CMPS0046", "ShaderWithRecursiveMembers"));
}
[TestMethod]
@@ -1523,7 +1523,7 @@ public void Execute()
}
""";
- CSharpGeneratorTest.VerifyDiagnostics(source, "CMPS0064");
+ CSharpGeneratorTest.VerifyDiagnostics(source, ("CMPS0064", "MyShader"));
}
[TestMethod]
@@ -1549,6 +1549,6 @@ public void Execute()
}
""";
- CSharpGeneratorTest.VerifyDiagnostics(source, "CMPS0065");
+ CSharpGeneratorTest.VerifyDiagnostics(source, ("CMPS0065", "RequiresDoublePrecisionSupport"));
}
}
\ No newline at end of file