From 5a8382160b96875429495fa12606f4847b40f1b6 Mon Sep 17 00:00:00 2001 From: Rick Brewster Date: Mon, 31 Aug 2026 07:48:35 -0700 Subject: [PATCH 1/3] Parallelize build-time shader compilation across all shaders Move FXC/DXC compilation out of the ForAttributeWithMetadataName transform callbacks (which the incremental driver invokes sequentially) into a dedicated node that warms the shared bytecode cache for all shaders in parallel. A join node (ordered after the warm-up by its dataflow edge) then folds each compiled shader back into its model and synthesizes the compile diagnostics, using location info captured by value during the transform. Co-Authored-By: Claude Fable 5 --- .../D2DPixelShaderDescriptorGenerator.cs | 75 +++++++++++-- .../Models/D2D1ShaderInfo.cs | 2 + ...mputeSharp.SourceGeneration.Hlsl.projitems | 1 + .../Models/HlslBytecodeDiagnosticsInfo.cs | 16 +++ .../HlslBytecodeSyntaxProcessor.cs | 103 +++++++++++++++--- .../ComputeSharp.SourceGeneration.projitems | 1 + .../Models/LocationInfo.cs | 53 +++++++++ .../ComputeShaderDescriptorGenerator.cs | 71 ++++++++++-- .../Models/ShaderInfo.cs | 2 + 9 files changed, 284 insertions(+), 40 deletions(-) create mode 100644 src/ComputeSharp.SourceGeneration.Hlsl/Models/HlslBytecodeDiagnosticsInfo.cs create mode 100644 src/ComputeSharp.SourceGeneration/Models/LocationInfo.cs diff --git a/src/ComputeSharp.D2D1.SourceGenerators/D2DPixelShaderDescriptorGenerator.cs b/src/ComputeSharp.D2D1.SourceGenerators/D2DPixelShaderDescriptorGenerator.cs index 1d1459093..afe73e24b 100644 --- a/src/ComputeSharp.D2D1.SourceGenerators/D2DPixelShaderDescriptorGenerator.cs +++ b/src/ComputeSharp.D2D1.SourceGenerators/D2DPixelShaderDescriptorGenerator.cs @@ -27,7 +27,8 @@ 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 = + // (with the exception of the compiled HLSL bytecode, which is processed later) + IncrementalValuesProvider shaderInfoWithNoHlslBytecode = context.SyntaxProvider .ForAttributeWithMetadataName( "ComputeSharp.D2D1.D2DGeneratedPixelShaderDescriptorAttribute", @@ -152,22 +153,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(); @@ -192,12 +190,65 @@ public void Initialize(IncrementalGeneratorInitializationContext context) ChannelDepth: channelDepth, PixelOptions: pixelOptions, HlslInfoKey: hlslInfoKey, - HlslInfo: hlslInfo, + HlslInfo: HlslBytecodeInfo.Missing.Instance, + HlslDiagnosticsInfo: hlslDiagnosticsInfo, Diagnostcs: diagnostics.ToImmutable()); }) .WithTrackingName(WellKnownTrackingNames.Execute) .Where(static item => item 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 = + shaderInfoWithNoHlslBytecode + .Select(static (item, _) => item.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 = + shaderInfoWithNoHlslBytecode + .Combine(hlslBytecodeCache) + .Select(static (pair, token) => + { + D2D1ShaderInfo item = pair.Left; + + HlslBytecodeInfoKey hlslInfoKey = item.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(item.Diagnostcs.AsSpan()); + + // Append any diagnostic for the shader compilation + HlslBytecodeSyntaxProcessor.GetInfoDiagnostics(item.HlslDiagnosticsInfo!, hlslInfo, diagnostics); + HlslBytecodeSyntaxProcessor.GetDoublePrecisionSupportDiagnostics(item.HlslDiagnosticsInfo!, hlslInfo, diagnostics); + + token.ThrowIfCancellationRequested(); + + // The diagnostics info is dropped here, as it has served its purpose. This also improves + // incrementality, as it holds a reference to the syntax tree of the shader type, which + // would otherwise cause spurious changes in the resulting models on unrelated edits. + return item with + { + HlslInfoKey = hlslInfoKey, + HlslInfo = hlslInfo, + HlslDiagnosticsInfo = null, + Diagnostcs = diagnostics.ToImmutable() + }; + }); + // 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. IncrementalValuesProvider> diagnosticInfo = diff --git a/src/ComputeSharp.D2D1.SourceGenerators/Models/D2D1ShaderInfo.cs b/src/ComputeSharp.D2D1.SourceGenerators/Models/D2D1ShaderInfo.cs index 31d5d1be6..65aa8c47f 100644 --- a/src/ComputeSharp.D2D1.SourceGenerators/Models/D2D1ShaderInfo.cs +++ b/src/ComputeSharp.D2D1.SourceGenerators/Models/D2D1ShaderInfo.cs @@ -22,6 +22,7 @@ namespace ComputeSharp.D2D1.SourceGenerators.Models; /// The pixel options used by the shader. /// The key with processed info on the shader. /// The value with processed info on the shader. +/// The captured info to synthesize diagnostics for the compiled shader (only present until the bytecode is processed). /// The discovered diagnostics, if any. internal sealed record D2D1ShaderInfo( HierarchyInfo Hierarchy, @@ -40,4 +41,5 @@ internal sealed record D2D1ShaderInfo( D2D1PixelOptions PixelOptions, HlslBytecodeInfoKey HlslInfoKey, HlslBytecodeInfo HlslInfo, + HlslBytecodeDiagnosticsInfo? HlslDiagnosticsInfo, EquatableArray Diagnostcs) : IConstantBufferInfo; \ No newline at end of file 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..f85d3dbf1 --- /dev/null +++ b/src/ComputeSharp.SourceGeneration.Hlsl/Models/HlslBytecodeDiagnosticsInfo.cs @@ -0,0 +1,16 @@ +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, +/// which in turn allows deferring the bytecode compilation (so it can be parallelized). +/// +/// 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 present. +internal sealed record HlslBytecodeDiagnosticsInfo( + string TypeName, + LocationInfo? TypeLocation, + bool HasRequiresDoublePrecisionSupportAttribute, + LocationInfo? RequiresDoublePrecisionSupportAttributeLocation); diff --git a/src/ComputeSharp.SourceGeneration.Hlsl/SyntaxProcessors/HlslBytecodeSyntaxProcessor.cs b/src/ComputeSharp.SourceGeneration.Hlsl/SyntaxProcessors/HlslBytecodeSyntaxProcessor.cs index f530496ee..a2b40b8e9 100644 --- a/src/ComputeSharp.SourceGeneration.Hlsl/SyntaxProcessors/HlslBytecodeSyntaxProcessor.cs +++ b/src/ComputeSharp.SourceGeneration.Hlsl/SyntaxProcessors/HlslBytecodeSyntaxProcessor.cs @@ -1,8 +1,10 @@ using System; using System.Collections.Immutable; using System.ComponentModel; +using System.Linq; using System.Runtime.CompilerServices; using System.Threading; +using System.Threading.Tasks; using ComputeSharp.SourceGeneration.Extensions; using ComputeSharp.SourceGeneration.Helpers; using ComputeSharp.SourceGeneration.Models; @@ -89,13 +91,82 @@ 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) + { + // 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(); + + 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.ToString(), + TypeLocation: LocationInfo.From(structDeclarationSymbol), + HasRequiresDoublePrecisionSupportAttribute: hasRequiresDoublePrecisionSupportAttribute, + RequiresDoublePrecisionSupportAttributeLocation: LocationInfo.From(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 +176,8 @@ public static void GetInfoDiagnostics( { diagnostic = DiagnosticInfo.Create( HlslBytecodeFailedWithWin32Exception, - structDeclarationSymbol, - structDeclarationSymbol, + diagnosticsInfo.TypeLocation?.ToLocation(), + diagnosticsInfo.TypeName, win32Error.HResult, win32Error.Message); } @@ -114,8 +185,8 @@ public static void GetInfoDiagnostics( { diagnostic = DiagnosticInfo.Create( HlslBytecodeFailedWithCompilationException, - structDeclarationSymbol, - structDeclarationSymbol, + diagnosticsInfo.TypeLocation?.ToLocation(), + diagnosticsInfo.TypeName, fxcError.Message); } @@ -128,11 +199,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 +213,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?.ToLocation(), + diagnosticsInfo.TypeName)); } - else if (hasRequiresDoublePrecisionSupportAttribute && !success.RequiresDoublePrecisionSupport) + else if (diagnosticsInfo.HasRequiresDoublePrecisionSupportAttribute && !success.RequiresDoublePrecisionSupport) { diagnostics.Add(DiagnosticInfo.Create( UnnecessaryRequiresDoublePrecisionSupportAttribute, - attributeData!.GetLocation(), - structDeclarationSymbol)); + (diagnosticsInfo.RequiresDoublePrecisionSupportAttributeLocation ?? diagnosticsInfo.TypeLocation)?.ToLocation(), + diagnosticsInfo.TypeName)); } } diff --git a/src/ComputeSharp.SourceGeneration/ComputeSharp.SourceGeneration.projitems b/src/ComputeSharp.SourceGeneration/ComputeSharp.SourceGeneration.projitems index b68d60007..1a705e1bd 100644 --- a/src/ComputeSharp.SourceGeneration/ComputeSharp.SourceGeneration.projitems +++ b/src/ComputeSharp.SourceGeneration/ComputeSharp.SourceGeneration.projitems @@ -32,6 +32,7 @@ + \ No newline at end of file diff --git a/src/ComputeSharp.SourceGeneration/Models/LocationInfo.cs b/src/ComputeSharp.SourceGeneration/Models/LocationInfo.cs new file mode 100644 index 000000000..b7a939e11 --- /dev/null +++ b/src/ComputeSharp.SourceGeneration/Models/LocationInfo.cs @@ -0,0 +1,53 @@ +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; + +namespace ComputeSharp.SourceGeneration.Models; + +/// +/// A model for a captured source location, to be used within equatable incremental models. +/// The location is captured by value (ie. with no references), so +/// that models with a captured location will correctly compare as equal across unrelated +/// edits (and so that they will never keep alive (or leak) any stale compilation objects). +/// +/// The path of the source file for the referenced location. +/// The span for the referenced location. +/// The line span for the referenced location. +internal sealed record LocationInfo(string FilePath, TextSpan TextSpan, LinePositionSpan LineSpan) +{ + /// + /// Creates a new instance from an input value. + /// + /// The value to capture, if available. + /// A instance for , if a source location was available. + public static LocationInfo? From(Location? location) + { + if (location is not { SourceTree: not null }) + { + return null; + } + + FileLinePositionSpan lineSpan = location.GetLineSpan(); + + return new LocationInfo(lineSpan.Path, location.SourceSpan, lineSpan.Span); + } + + /// + /// Creates a new instance from an input value. + /// + /// The instance to capture the location for. + /// A instance for , if a source location was available. + public static LocationInfo? From(ISymbol symbol) + { + return From(symbol.Locations.FirstOrDefault()); + } + + /// + /// Creates a new instance with the state from this model. + /// + /// A new instance with the state from this model. + public Location ToLocation() + { + return Location.Create(FilePath, TextSpan, LineSpan); + } +} diff --git a/src/ComputeSharp.SourceGenerators/ComputeShaderDescriptorGenerator.cs b/src/ComputeSharp.SourceGenerators/ComputeShaderDescriptorGenerator.cs index 1ed999402..427a2361b 100644 --- a/src/ComputeSharp.SourceGenerators/ComputeShaderDescriptorGenerator.cs +++ b/src/ComputeSharp.SourceGenerators/ComputeShaderDescriptorGenerator.cs @@ -27,7 +27,8 @@ 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 = + // (with the exception of the compiled HLSL bytecode, which is processed later) + IncrementalValuesProvider shaderInfoWithNoHlslBytecode = 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(); @@ -147,12 +147,63 @@ public void Initialize(IncrementalGeneratorInitializationContext context) Resources: resourceInfo, ResourceDescriptors: resourceDescriptors, HlslInfoKey: hlslInfoKey, - HlslInfo: hlslInfo, + HlslInfo: HlslBytecodeInfo.Missing.Instance, + HlslDiagnosticsInfo: hlslDiagnosticsInfo, Diagnostcs: diagnostics.ToImmutable()); }) .WithTrackingName(WellKnownTrackingNames.Execute) .Where(static item => item 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 = + shaderInfoWithNoHlslBytecode + .Select(static (item, _) => item.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 = + shaderInfoWithNoHlslBytecode + .Combine(hlslBytecodeCache) + .Select(static (pair, token) => + { + ShaderInfo item = pair.Left; + + HlslBytecodeInfoKey hlslInfoKey = item.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(item.Diagnostcs.AsSpan()); + + // Append any diagnostic for the shader compilation + HlslBytecodeSyntaxProcessor.GetInfoDiagnostics(item.HlslDiagnosticsInfo!, hlslInfo, diagnostics); + HlslBytecodeSyntaxProcessor.GetDoublePrecisionSupportDiagnostics(item.HlslDiagnosticsInfo!, hlslInfo, diagnostics); + + token.ThrowIfCancellationRequested(); + + // The diagnostics info is dropped here, as it has served its purpose (see notes in the D2D1 generator) + return item with + { + HlslInfoKey = hlslInfoKey, + HlslInfo = hlslInfo, + HlslDiagnosticsInfo = null, + Diagnostcs = diagnostics.ToImmutable() + }; + }); + // Split the diagnostics, and drop them from the output provider (see more notes in the D2D1 generator) IncrementalValuesProvider> diagnosticInfo = shaderInfo diff --git a/src/ComputeSharp.SourceGenerators/Models/ShaderInfo.cs b/src/ComputeSharp.SourceGenerators/Models/ShaderInfo.cs index c26175372..f870e2b21 100644 --- a/src/ComputeSharp.SourceGenerators/Models/ShaderInfo.cs +++ b/src/ComputeSharp.SourceGenerators/Models/ShaderInfo.cs @@ -18,6 +18,7 @@ namespace ComputeSharp.SourceGenerators.Models; /// The sequence of resource descriptors for the shader. /// The key with processed info on the shader. /// The value with processed info on the shader. +/// The captured info to synthesize diagnostics for the compiled shader (only present until the bytecode is processed). /// The discovered diagnostics, if any. internal sealed record ShaderInfo( HierarchyInfo Hierarchy, @@ -32,4 +33,5 @@ internal sealed record ShaderInfo( EquatableArray ResourceDescriptors, HlslBytecodeInfoKey HlslInfoKey, HlslBytecodeInfo HlslInfo, + HlslBytecodeDiagnosticsInfo? HlslDiagnosticsInfo, EquatableArray Diagnostcs) : IConstantBufferInfo; \ No newline at end of file From 72f3c7edaf84114a36dca2671abb247eedb1b0f7 Mon Sep 17 00:00:00 2001 From: Rick Brewster Date: Fri, 18 Sep 2026 09:21:45 -0700 Subject: [PATCH 2/3] Preserve source locations for deferred shader compilation diagnostics The deferred compile diagnostics (CMPSD2D0033/0034/0080/0081 and their D3D12 counterparts) were reported with no location, because their by-value LocationInfo produced an external-file Location whose SyntaxTree is null, which DiagnosticInfo then dropped entirely. This also broke #pragma and [SuppressMessage] suppression for the one warning-severity diagnostic. The transform now returns the shader model alongside a separate HlslBytecodeDiagnosticsInfo holding the real Location values, which only travels up to the join node that creates the diagnostics. The models are back to their original shape, and the "Execute" tracking name moves to the join, which is the node producing the complete model with diagnostics (all existing incrementality expectations hold unchanged). Also unwrap a single unexpected exception from the parallel warm-up (with ExceptionDispatchInfo, preserving its stack trace) so that CS8785 reports the actual exception type, and add tests asserting diagnostic locations. Co-Authored-By: Claude Fable 5.1 --- .../D2DPixelShaderDescriptorGenerator.cs | 46 ++++++++-------- .../Models/D2D1ShaderInfo.cs | 2 - .../Models/HlslBytecodeDiagnosticsInfo.cs | 16 +++--- .../HlslBytecodeSyntaxProcessor.cs | 22 +++++--- .../ComputeSharp.SourceGeneration.projitems | 1 - .../Models/LocationInfo.cs | 53 ------------------- .../ComputeShaderDescriptorGenerator.cs | 37 +++++++------ .../Models/ShaderInfo.cs | 2 - ...elShaderDescriptorGenerator_Diagnostics.cs | 47 +++++++++++++++- .../CSharpGeneratorTest{TGenerator}.cs | 30 +++++++++++ ...teShaderDescriptorGenerator_Diagnostics.cs | 6 +-- 11 files changed, 144 insertions(+), 118 deletions(-) delete mode 100644 src/ComputeSharp.SourceGeneration/Models/LocationInfo.cs diff --git a/src/ComputeSharp.D2D1.SourceGenerators/D2DPixelShaderDescriptorGenerator.cs b/src/ComputeSharp.D2D1.SourceGenerators/D2DPixelShaderDescriptorGenerator.cs index afe73e24b..0ebdc716e 100644 --- a/src/ComputeSharp.D2D1.SourceGenerators/D2DPixelShaderDescriptorGenerator.cs +++ b/src/ComputeSharp.D2D1.SourceGenerators/D2DPixelShaderDescriptorGenerator.cs @@ -26,9 +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 - // (with the exception of the compiled HLSL bytecode, which is processed later) - IncrementalValuesProvider shaderInfoWithNoHlslBytecode = + // 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", @@ -174,7 +176,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) token.ThrowIfCancellationRequested(); - return new D2D1ShaderInfo( + D2D1ShaderInfo info = new( Hierarchy: hierarchyInfo, EffectId: effectId, EffectDisplayName: effectDisplayName, @@ -191,18 +193,18 @@ public void Initialize(IncrementalGeneratorInitializationContext context) PixelOptions: pixelOptions, HlslInfoKey: hlslInfoKey, HlslInfo: HlslBytecodeInfo.Missing.Instance, - HlslDiagnosticsInfo: hlslDiagnosticsInfo, 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 = - shaderInfoWithNoHlslBytecode - .Select(static (item, _) => item.HlslInfoKey) + shaderInfoWithDiagnosticsInfo + .Select(static (item, _) => item.Info.HlslInfoKey) .Collect() .Select(static (keys, token) => { @@ -211,16 +213,17 @@ public void Initialize(IncrementalGeneratorInitializationContext context) 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 + // 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 = - shaderInfoWithNoHlslBytecode + shaderInfoWithDiagnosticsInfo .Combine(hlslBytecodeCache) .Select(static (pair, token) => { - D2D1ShaderInfo item = pair.Left; + (D2D1ShaderInfo info, HlslBytecodeDiagnosticsInfo diagnosticsInfo) = pair.Left; - HlslBytecodeInfoKey hlslInfoKey = item.HlslInfoKey; + HlslBytecodeInfoKey hlslInfoKey = info.HlslInfoKey; // Get the compiled shader from the warmed up cache HlslBytecodeInfo hlslInfo = HlslBytecodeSyntaxProcessor.GetInfo(ref hlslInfoKey, token); @@ -229,25 +232,22 @@ public void Initialize(IncrementalGeneratorInitializationContext context) using ImmutableArrayBuilder diagnostics = new(); - diagnostics.AddRange(item.Diagnostcs.AsSpan()); + diagnostics.AddRange(info.Diagnostcs.AsSpan()); // Append any diagnostic for the shader compilation - HlslBytecodeSyntaxProcessor.GetInfoDiagnostics(item.HlslDiagnosticsInfo!, hlslInfo, diagnostics); - HlslBytecodeSyntaxProcessor.GetDoublePrecisionSupportDiagnostics(item.HlslDiagnosticsInfo!, hlslInfo, diagnostics); + HlslBytecodeSyntaxProcessor.GetInfoDiagnostics(diagnosticsInfo, hlslInfo, diagnostics); + HlslBytecodeSyntaxProcessor.GetDoublePrecisionSupportDiagnostics(diagnosticsInfo, hlslInfo, diagnostics); token.ThrowIfCancellationRequested(); - // The diagnostics info is dropped here, as it has served its purpose. This also improves - // incrementality, as it holds a reference to the syntax tree of the shader type, which - // would otherwise cause spurious changes in the resulting models on unrelated edits. - return item with + return info with { HlslInfoKey = hlslInfoKey, HlslInfo = hlslInfo, - HlslDiagnosticsInfo = null, 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.D2D1.SourceGenerators/Models/D2D1ShaderInfo.cs b/src/ComputeSharp.D2D1.SourceGenerators/Models/D2D1ShaderInfo.cs index 65aa8c47f..31d5d1be6 100644 --- a/src/ComputeSharp.D2D1.SourceGenerators/Models/D2D1ShaderInfo.cs +++ b/src/ComputeSharp.D2D1.SourceGenerators/Models/D2D1ShaderInfo.cs @@ -22,7 +22,6 @@ namespace ComputeSharp.D2D1.SourceGenerators.Models; /// The pixel options used by the shader. /// The key with processed info on the shader. /// The value with processed info on the shader. -/// The captured info to synthesize diagnostics for the compiled shader (only present until the bytecode is processed). /// The discovered diagnostics, if any. internal sealed record D2D1ShaderInfo( HierarchyInfo Hierarchy, @@ -41,5 +40,4 @@ internal sealed record D2D1ShaderInfo( D2D1PixelOptions PixelOptions, HlslBytecodeInfoKey HlslInfoKey, HlslBytecodeInfo HlslInfo, - HlslBytecodeDiagnosticsInfo? HlslDiagnosticsInfo, EquatableArray Diagnostcs) : IConstantBufferInfo; \ No newline at end of file diff --git a/src/ComputeSharp.SourceGeneration.Hlsl/Models/HlslBytecodeDiagnosticsInfo.cs b/src/ComputeSharp.SourceGeneration.Hlsl/Models/HlslBytecodeDiagnosticsInfo.cs index f85d3dbf1..6d8b4d827 100644 --- a/src/ComputeSharp.SourceGeneration.Hlsl/Models/HlslBytecodeDiagnosticsInfo.cs +++ b/src/ComputeSharp.SourceGeneration.Hlsl/Models/HlslBytecodeDiagnosticsInfo.cs @@ -1,16 +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, -/// which in turn allows deferring the bytecode compilation (so it can be parallelized). +/// 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 present. +/// The location of the attribute requiring double precision support, if available. internal sealed record HlslBytecodeDiagnosticsInfo( string TypeName, - LocationInfo? TypeLocation, + Location? TypeLocation, bool HasRequiresDoublePrecisionSupportAttribute, - LocationInfo? RequiresDoublePrecisionSupportAttributeLocation); + Location? RequiresDoublePrecisionSupportAttributeLocation); diff --git a/src/ComputeSharp.SourceGeneration.Hlsl/SyntaxProcessors/HlslBytecodeSyntaxProcessor.cs b/src/ComputeSharp.SourceGeneration.Hlsl/SyntaxProcessors/HlslBytecodeSyntaxProcessor.cs index a2b40b8e9..146d1222a 100644 --- a/src/ComputeSharp.SourceGeneration.Hlsl/SyntaxProcessors/HlslBytecodeSyntaxProcessor.cs +++ b/src/ComputeSharp.SourceGeneration.Hlsl/SyntaxProcessors/HlslBytecodeSyntaxProcessor.cs @@ -3,6 +3,7 @@ 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; @@ -128,13 +129,20 @@ static void Compile(HlslBytecodeInfoKey key, CancellationToken token) new ParallelOptions { CancellationToken = token }, key => Compile(key, token)); } - catch (AggregateException) + 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; } } @@ -154,9 +162,9 @@ public static HlslBytecodeDiagnosticsInfo GetDiagnosticsInfo(INamedTypeSymbol st return new HlslBytecodeDiagnosticsInfo( TypeName: structDeclarationSymbol.ToString(), - TypeLocation: LocationInfo.From(structDeclarationSymbol), + TypeLocation: structDeclarationSymbol.Locations.FirstOrDefault(), HasRequiresDoublePrecisionSupportAttribute: hasRequiresDoublePrecisionSupportAttribute, - RequiresDoublePrecisionSupportAttributeLocation: LocationInfo.From(attributeData?.GetLocation())); + RequiresDoublePrecisionSupportAttributeLocation: attributeData?.GetLocation()); } /// @@ -176,7 +184,7 @@ public static void GetInfoDiagnostics( { diagnostic = DiagnosticInfo.Create( HlslBytecodeFailedWithWin32Exception, - diagnosticsInfo.TypeLocation?.ToLocation(), + diagnosticsInfo.TypeLocation, diagnosticsInfo.TypeName, win32Error.HResult, win32Error.Message); @@ -185,7 +193,7 @@ public static void GetInfoDiagnostics( { diagnostic = DiagnosticInfo.Create( HlslBytecodeFailedWithCompilationException, - diagnosticsInfo.TypeLocation?.ToLocation(), + diagnosticsInfo.TypeLocation, diagnosticsInfo.TypeName, fxcError.Message); } @@ -220,14 +228,14 @@ public static void GetDoublePrecisionSupportDiagnostics( { diagnostics.Add(DiagnosticInfo.Create( MissingRequiresDoublePrecisionSupportAttribute, - diagnosticsInfo.TypeLocation?.ToLocation(), + diagnosticsInfo.TypeLocation, diagnosticsInfo.TypeName)); } else if (diagnosticsInfo.HasRequiresDoublePrecisionSupportAttribute && !success.RequiresDoublePrecisionSupport) { diagnostics.Add(DiagnosticInfo.Create( UnnecessaryRequiresDoublePrecisionSupportAttribute, - (diagnosticsInfo.RequiresDoublePrecisionSupportAttributeLocation ?? diagnosticsInfo.TypeLocation)?.ToLocation(), + diagnosticsInfo.RequiresDoublePrecisionSupportAttributeLocation, diagnosticsInfo.TypeName)); } } diff --git a/src/ComputeSharp.SourceGeneration/ComputeSharp.SourceGeneration.projitems b/src/ComputeSharp.SourceGeneration/ComputeSharp.SourceGeneration.projitems index 1a705e1bd..b68d60007 100644 --- a/src/ComputeSharp.SourceGeneration/ComputeSharp.SourceGeneration.projitems +++ b/src/ComputeSharp.SourceGeneration/ComputeSharp.SourceGeneration.projitems @@ -32,7 +32,6 @@ - \ No newline at end of file diff --git a/src/ComputeSharp.SourceGeneration/Models/LocationInfo.cs b/src/ComputeSharp.SourceGeneration/Models/LocationInfo.cs deleted file mode 100644 index b7a939e11..000000000 --- a/src/ComputeSharp.SourceGeneration/Models/LocationInfo.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System.Linq; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Text; - -namespace ComputeSharp.SourceGeneration.Models; - -/// -/// A model for a captured source location, to be used within equatable incremental models. -/// The location is captured by value (ie. with no references), so -/// that models with a captured location will correctly compare as equal across unrelated -/// edits (and so that they will never keep alive (or leak) any stale compilation objects). -/// -/// The path of the source file for the referenced location. -/// The span for the referenced location. -/// The line span for the referenced location. -internal sealed record LocationInfo(string FilePath, TextSpan TextSpan, LinePositionSpan LineSpan) -{ - /// - /// Creates a new instance from an input value. - /// - /// The value to capture, if available. - /// A instance for , if a source location was available. - public static LocationInfo? From(Location? location) - { - if (location is not { SourceTree: not null }) - { - return null; - } - - FileLinePositionSpan lineSpan = location.GetLineSpan(); - - return new LocationInfo(lineSpan.Path, location.SourceSpan, lineSpan.Span); - } - - /// - /// Creates a new instance from an input value. - /// - /// The instance to capture the location for. - /// A instance for , if a source location was available. - public static LocationInfo? From(ISymbol symbol) - { - return From(symbol.Locations.FirstOrDefault()); - } - - /// - /// Creates a new instance with the state from this model. - /// - /// A new instance with the state from this model. - public Location ToLocation() - { - return Location.Create(FilePath, TextSpan, LineSpan); - } -} diff --git a/src/ComputeSharp.SourceGenerators/ComputeShaderDescriptorGenerator.cs b/src/ComputeSharp.SourceGenerators/ComputeShaderDescriptorGenerator.cs index 427a2361b..c614ac592 100644 --- a/src/ComputeSharp.SourceGenerators/ComputeShaderDescriptorGenerator.cs +++ b/src/ComputeSharp.SourceGenerators/ComputeShaderDescriptorGenerator.cs @@ -26,9 +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 - // (with the exception of the compiled HLSL bytecode, which is processed later) - IncrementalValuesProvider shaderInfoWithNoHlslBytecode = + // 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", @@ -135,7 +135,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) token.ThrowIfCancellationRequested(); - return new ShaderInfo( + ShaderInfo info = new( Hierarchy: hierarchyInfo, ThreadsX: threadsX, ThreadsY: threadsY, @@ -148,18 +148,18 @@ public void Initialize(IncrementalGeneratorInitializationContext context) ResourceDescriptors: resourceDescriptors, HlslInfoKey: hlslInfoKey, HlslInfo: HlslBytecodeInfo.Missing.Instance, - HlslDiagnosticsInfo: hlslDiagnosticsInfo, 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 = - shaderInfoWithNoHlslBytecode - .Select(static (item, _) => item.HlslInfoKey) + shaderInfoWithDiagnosticsInfo + .Select(static (item, _) => item.Info.HlslInfoKey) .Collect() .Select(static (keys, token) => { @@ -171,13 +171,13 @@ public void Initialize(IncrementalGeneratorInitializationContext context) // 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 = - shaderInfoWithNoHlslBytecode + shaderInfoWithDiagnosticsInfo .Combine(hlslBytecodeCache) .Select(static (pair, token) => { - ShaderInfo item = pair.Left; + (ShaderInfo info, HlslBytecodeDiagnosticsInfo diagnosticsInfo) = pair.Left; - HlslBytecodeInfoKey hlslInfoKey = item.HlslInfoKey; + HlslBytecodeInfoKey hlslInfoKey = info.HlslInfoKey; // Get the compiled shader from the warmed up cache HlslBytecodeInfo hlslInfo = HlslBytecodeSyntaxProcessor.GetInfo(ref hlslInfoKey, token); @@ -186,23 +186,22 @@ public void Initialize(IncrementalGeneratorInitializationContext context) using ImmutableArrayBuilder diagnostics = new(); - diagnostics.AddRange(item.Diagnostcs.AsSpan()); + diagnostics.AddRange(info.Diagnostcs.AsSpan()); // Append any diagnostic for the shader compilation - HlslBytecodeSyntaxProcessor.GetInfoDiagnostics(item.HlslDiagnosticsInfo!, hlslInfo, diagnostics); - HlslBytecodeSyntaxProcessor.GetDoublePrecisionSupportDiagnostics(item.HlslDiagnosticsInfo!, hlslInfo, diagnostics); + HlslBytecodeSyntaxProcessor.GetInfoDiagnostics(diagnosticsInfo, hlslInfo, diagnostics); + HlslBytecodeSyntaxProcessor.GetDoublePrecisionSupportDiagnostics(diagnosticsInfo, hlslInfo, diagnostics); token.ThrowIfCancellationRequested(); - // The diagnostics info is dropped here, as it has served its purpose (see notes in the D2D1 generator) - return item with + return info with { HlslInfoKey = hlslInfoKey, HlslInfo = hlslInfo, - HlslDiagnosticsInfo = null, 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/src/ComputeSharp.SourceGenerators/Models/ShaderInfo.cs b/src/ComputeSharp.SourceGenerators/Models/ShaderInfo.cs index f870e2b21..c26175372 100644 --- a/src/ComputeSharp.SourceGenerators/Models/ShaderInfo.cs +++ b/src/ComputeSharp.SourceGenerators/Models/ShaderInfo.cs @@ -18,7 +18,6 @@ namespace ComputeSharp.SourceGenerators.Models; /// The sequence of resource descriptors for the shader. /// The key with processed info on the shader. /// The value with processed info on the shader. -/// The captured info to synthesize diagnostics for the compiled shader (only present until the bytecode is processed). /// The discovered diagnostics, if any. internal sealed record ShaderInfo( HierarchyInfo Hierarchy, @@ -33,5 +32,4 @@ internal sealed record ShaderInfo( EquatableArray ResourceDescriptors, HlslBytecodeInfoKey HlslInfoKey, HlslBytecodeInfo HlslInfo, - HlslBytecodeDiagnosticsInfo? HlslDiagnosticsInfo, EquatableArray Diagnostcs) : IConstantBufferInfo; \ No newline at end of file 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 1d29bfafb..78888ac10 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 From a09494c62ae8a1165442cbff12ad22708724724b Mon Sep 17 00:00:00 2001 From: Rick Brewster Date: Fri, 18 Sep 2026 09:32:46 -0700 Subject: [PATCH 3/3] Fix nullable error with ToString() --- .../SyntaxProcessors/HlslBytecodeSyntaxProcessor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ComputeSharp.SourceGeneration.Hlsl/SyntaxProcessors/HlslBytecodeSyntaxProcessor.cs b/src/ComputeSharp.SourceGeneration.Hlsl/SyntaxProcessors/HlslBytecodeSyntaxProcessor.cs index 146d1222a..91c2d175e 100644 --- a/src/ComputeSharp.SourceGeneration.Hlsl/SyntaxProcessors/HlslBytecodeSyntaxProcessor.cs +++ b/src/ComputeSharp.SourceGeneration.Hlsl/SyntaxProcessors/HlslBytecodeSyntaxProcessor.cs @@ -161,7 +161,7 @@ public static HlslBytecodeDiagnosticsInfo GetDiagnosticsInfo(INamedTypeSymbol st out AttributeData? attributeData); return new HlslBytecodeDiagnosticsInfo( - TypeName: structDeclarationSymbol.ToString(), + TypeName: structDeclarationSymbol.ToDisplayString(), TypeLocation: structDeclarationSymbol.Locations.FirstOrDefault(), HasRequiresDoublePrecisionSupportAttribute: hasRequiresDoublePrecisionSupportAttribute, RequiresDoublePrecisionSupportAttributeLocation: attributeData?.GetLocation());