Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add diagnostic for invalid linking D2D compile option #638

Merged
merged 3 commits into from
Nov 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,4 @@ CMPSD2D0065 | ComputeSharp.D2D1.Shaders | Warning | [Documentation](https://gith
CMPSD2D0066 | ComputeSharp.D2D1.Shaders | Error | [Documentation](https://github.com/Sergio0694/ComputeSharp)
CMPSD2D0067 | ComputeSharp.D2D1.Shaders | Error | [Documentation](https://github.com/Sergio0694/ComputeSharp)
CMPSD2D0068 | ComputeSharp.D2D1.Shaders | Error | [Documentation](https://github.com/Sergio0694/ComputeSharp)
CMPSD2D0069 | ComputeSharp.D2D1.Shaders | Warning | [Documentation](https://github.com/Sergio0694/ComputeSharp)
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
using System;
using System.Collections.Immutable;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using ComputeSharp.D2D1.Shaders.Translation;
Expand Down Expand Up @@ -113,34 +112,71 @@ public static D2D1CompileOptions GetEffectiveCompileOptions(D2D1CompileOptions?
/// <summary>
/// Extracts the metadata definition for the current shader.
/// </summary>
/// <param name="compilation">The input <see cref="Compilation"/> object currently in use.</param>
/// <param name="structDeclarationSymbol">The input <see cref="INamedTypeSymbol"/> instance to process.</param>
/// <param name="inputCount">The number of inputs for the shader.</param>
/// <returns>Whether the shader only has simple inputs.</returns>
public static bool IsSimpleInputShader(INamedTypeSymbol structDeclarationSymbol, int inputCount)
public static bool IsSimpleInputShader(
Compilation compilation,
INamedTypeSymbol structDeclarationSymbol,
int inputCount)
{
return IsSimpleInputShader(
structDeclarationSymbol,
compilation.GetTypeByMetadataName("ComputeSharp.D2D1.D2DInputSimpleAttribute")!,
inputCount);
}

/// <summary>
/// Extracts the metadata definition for the current shader.
/// </summary>
/// <param name="structDeclarationSymbol">The input <see cref="INamedTypeSymbol"/> instance to process.</param>
/// <param name="d2DInputSimpleSymbolAttributeSymbol">The symbol for the <c>[D2DInputSimple]</c> attribute.</param>
/// <param name="inputCount">The number of inputs for the shader.</param>
/// <returns>Whether the shader only has simple inputs.</returns>
public static bool IsSimpleInputShader(
INamedTypeSymbol structDeclarationSymbol,
INamedTypeSymbol d2DInputSimpleSymbolAttributeSymbol,
int inputCount)
{
// We cannot trust the input count to be valid at this point (it may be invalid and
// with diagnostic already emitted for it). So first, just clamp it in the right range.
inputCount = Math.Max(0, Math.Min(8, inputCount));

// If there are no inputs, the shader is as if only had simple inputs
if (inputCount == 0)
{
return true;
}

// Build a map of all simple inputs (unmarked inputs default to being complex)
bool[] simpleInputsMap = new bool[inputCount];
// Build a map of all simple inputs (unmarked inputs default to being complex).
// We can never have more than 8 inputs, and if there are it means the shader is
// not valid. Just ignore them, and the generator will emit a separate diagnostic.
Span<bool> simpleInputsMap = stackalloc bool[8];

// We first start with all inputs marked as complex (ie. not simple)
simpleInputsMap.Clear();

foreach (AttributeData attributeData in structDeclarationSymbol.GetAttributes())
{
switch (attributeData.AttributeClass?.GetFullyQualifiedMetadataName())
// Only retrieve indices of simple inputs that are in range. If an input is out of
// range, the diagnostic for it will already be emitted by a previous generator step.
if (SymbolEqualityComparer.Default.Equals(attributeData.AttributeClass, d2DInputSimpleSymbolAttributeSymbol) &&
attributeData.ConstructorArguments is [{ Value: >= 0 and < 8 and int index }])
{
// Only retrieve indices of simple inputs that are in range. If an input is out of
// range, the diagnostic for it will already be emitted by a previous generator step.
case "ComputeSharp.D2D1.D2DInputSimpleAttribute"
when attributeData.ConstructorArguments[0].Value is int index && index < inputCount:
simpleInputsMap[index] = true;
break;
simpleInputsMap[index] = true;
}
}

return simpleInputsMap.All(static x => x);
bool isSimpleInputShader = true;

// Validate all inputs in our range (filtered by the allowed one)
foreach (bool isSimpleInput in simpleInputsMap[..inputCount])
{
isSimpleInputShader &= isSimpleInput;
}

return isSimpleInputShader;
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
token.ThrowIfCancellationRequested();

// Get the shader profile and linking info for LoadBytecode()
bool isLinkingSupported = HlslBytecode.IsSimpleInputShader(typeSymbol, inputCount);
bool isLinkingSupported = HlslBytecode.IsSimpleInputShader(context.SemanticModel.Compilation, typeSymbol, inputCount);
D2D1ShaderProfile? requestedShaderProfile = HlslBytecode.GetRequestedShaderProfile(typeSymbol);
D2D1CompileOptions? requestedCompileOptions = HlslBytecode.GetRequestedCompileOptions(diagnostics, typeSymbol);
D2D1ShaderProfile effectiveShaderProfile = HlslBytecode.GetEffectiveShaderProfile(requestedShaderProfile, out bool isCompilationEnabled);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
using System.Collections.Immutable;
using ComputeSharp.SourceGeneration.Extensions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using static ComputeSharp.SourceGeneration.Diagnostics.DiagnosticDescriptors;

namespace ComputeSharp.D2D1.SourceGenerators;

/// <summary>
/// A diagnostic analyzer that generates an error whenever [D2DCompileOptions] is used on a shader type to request linking when not supported.
/// </summary>
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class InvalidD2D1CompileOptionsEnableLinkingOnShaderTypeAnalyzer : DiagnosticAnalyzer
{
/// <inheritdoc/>
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(InvalidD2D1CompileOptionsEnableLinkingOnShaderType);

/// <inheritdoc/>
public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics);
context.EnableConcurrentExecution();

context.RegisterCompilationStartAction(static context =>
{
// Get the [D2DCompileOptions], [D2DInputCount] and [D2DInputSimple] symbols
if (context.Compilation.GetTypeByMetadataName("ComputeSharp.D2D1.D2DCompileOptionsAttribute") is not { } d2DCompileOptionsAttributeSymbol ||
context.Compilation.GetTypeByMetadataName("ComputeSharp.D2D1.D2DInputCountAttribute") is not { } d2DInputCountAttributeSymbol ||
context.Compilation.GetTypeByMetadataName("ComputeSharp.D2D1.D2DInputSimpleAttribute") is not { } d2DInputSimpleAttributeSymbol)
{
return;
}

context.RegisterSymbolAction(context =>
{
// Only struct types are possible targets
if (context.Symbol is not INamedTypeSymbol { TypeKind: TypeKind.Struct } typeSymbol)
{
return;
}

// If the type is not using [D2DCompileOptions] with D2D1CompileOptions.EnableLinking, there's nothing to do
if (!typeSymbol.TryGetAttributeWithType(d2DCompileOptionsAttributeSymbol, out AttributeData? compileOptionsAttribute) ||
!((D2D1CompileOptions)compileOptionsAttribute.ConstructorArguments[0].Value!).HasFlag(D2D1CompileOptions.EnableLinking))
{
return;
}

// Make sure we have the [D2DInputCount] (if not present, the shader is invalid anyway) and with a valid value
if (!typeSymbol.TryGetAttributeWithType(d2DInputCountAttributeSymbol, out AttributeData? inputCountAttribute) ||
inputCountAttribute.ConstructorArguments is not [{ Value: >= 0 and < 8 and int inputCount }])
{
return;
}

// Emit a diagnostic if the compile options are not valid for the shader type
if (!D2DPixelShaderDescriptorGenerator.HlslBytecode.IsSimpleInputShader(typeSymbol, d2DInputSimpleAttributeSymbol, inputCount))
{
context.ReportDiagnostic(Diagnostic.Create(
InvalidD2D1CompileOptionsEnableLinkingOnShaderType,
compileOptionsAttribute.GetLocation(),
typeSymbol));
}
}, SymbolKind.NamedType);
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1022,4 +1022,20 @@ partial class DiagnosticDescriptors
isEnabledByDefault: true,
description: "The [D2DGeneratedPixelShaderDescriptor] attribute requires the type of all fields of target types to be accessible from their containing assembly.",
helpLinkUri: "https://github.com/Sergio0694/ComputeSharp");

/// <summary>
/// Gets a <see cref="DiagnosticDescriptor"/> for an invalid use of <c>[D2DCompileOptions]</c> requesting to enable linking.
/// <para>
/// Format: <c>"The D2D1 shader of type {0} cannot use D2D1CompileOptions.EnableLinking in its [D2DCompileOptions] attribute, as it doesn't support linking (only D2D1 shaders with no complex inputs can use this option)"</c>.
/// </para>
/// </summary>
public static readonly DiagnosticDescriptor InvalidD2D1CompileOptionsEnableLinkingOnShaderType = new(
id: "CMPSD2D0069",
title: "Invalid [D2DResourceTextureIndex] use",
messageFormat: """The D2D1 shader of type {0} cannot use D2D1CompileOptions.EnableLinking in its [D2DCompileOptions] attribute, as it doesn't support linking (only D2D1 shaders with no complex inputs can use this option)""",
category: "ComputeSharp.D2D1.Shaders",
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true,
description: "A D2D1 shader cannot use D2D1CompileOptions.EnableLinking in its [D2DCompileOptions] attribute if it doesn't support linking (only D2D1 shaders with no complex inputs can use this option).",
helpLinkUri: "https://github.com/Sergio0694/ComputeSharp");
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
https://api.nuget.org/v3/index.json;
https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-tools/nuget/v3/index.json;
</RestoreSources>
<DefineConstants>$(DefineConstants);D2D1_TESTS</DefineConstants>
</PropertyGroup>

<ItemGroup>
Expand All @@ -23,4 +24,8 @@
<ProjectReference Include="..\..\src\ComputeSharp.D2D1.CodeFixers\ComputeSharp.D2D1.CodeFixers.csproj" />
<ProjectReference Include="..\..\src\ComputeSharp.D2D1.SourceGenerators\ComputeSharp.D2D1.SourceGenerators.csproj" />
</ItemGroup>

<ItemGroup>
<Compile Include="..\ComputeSharp.Tests.SourceGenerators\Helpers\CSharpAnalyzerWithLanguageVersionTest{TAnalyzer}.cs" Link="Helpers\CSharpAnalyzerWithLanguageVersionTest{TAnalyzer}.cs" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
using System.Threading.Tasks;
using ComputeSharp.D2D1.SourceGenerators;
using ComputeSharp.Tests.SourceGenerators.Helpers;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace ComputeSharp.D2D1.Tests.SourceGenerators;

[TestClass]
public class Test_InvalidD2D1CompileOptionsEnableLinkingOnShaderTypeAnalyzer
{
[TestMethod]
public async Task NoCompileOptionsDoesNotWarn()
{
const string source = """
using ComputeSharp;
using ComputeSharp.D2D1;

public partial class Foo
{
[D2DInputCount(0)]
private partial struct MyShader : ID2D1PixelShader
{
public Float4 Execute() => 0;
}
}
""";

await CSharpAnalyzerWithLanguageVersionTest<InvalidD2D1CompileOptionsEnableLinkingOnShaderTypeAnalyzer>.VerifyAnalyzerAsync(source);
}

[TestMethod]
public async Task ValidCompileOptionsDoesNotWarn()
{
const string source = """
using ComputeSharp;
using ComputeSharp.D2D1;

public partial class Foo
{
[D2DInputCount(0)]
[D2DCompileOptions(D2D1CompileOptions.PackMatrixRowMajor)]
private partial struct MyShader : ID2D1PixelShader
{
public Float4 Execute() => 0;
}
}
""";

await CSharpAnalyzerWithLanguageVersionTest<InvalidD2D1CompileOptionsEnableLinkingOnShaderTypeAnalyzer>.VerifyAnalyzerAsync(source);
}

[TestMethod]
public async Task EnableLinkingWithNoInputCountDoesNotWarn()
{
const string source = """
using ComputeSharp;
using ComputeSharp.D2D1;

public partial class Foo
{
[D2DInputComplex(0)]
[D2DCompileOptions(D2D1CompileOptions.EnableLinking)]
private partial struct MyShader : ID2D1PixelShader
{
public Float4 Execute() => 0;
}
}
""";

await CSharpAnalyzerWithLanguageVersionTest<InvalidD2D1CompileOptionsEnableLinkingOnShaderTypeAnalyzer>.VerifyAnalyzerAsync(source);
}

[TestMethod]
public async Task EnableLinkingWithImplicitComplexInputWarns()
{
const string source = """
using ComputeSharp;
using ComputeSharp.D2D1;

public partial class Foo
{
[D2DInputCount(2)]
[D2DInputSimple(1)]
[{|CMPSD2D0069:D2DCompileOptions(D2D1CompileOptions.Default | D2D1CompileOptions.EnableLinking)|}]
private partial struct MyShader : ID2D1PixelShader
{
public Float4 Execute() => 0;
}
}
""";

await CSharpAnalyzerWithLanguageVersionTest<InvalidD2D1CompileOptionsEnableLinkingOnShaderTypeAnalyzer>.VerifyAnalyzerAsync(source);
}

[TestMethod]
public async Task EnableLinkingWithExplicitComplexInputWarns()
{
const string source = """
using ComputeSharp;
using ComputeSharp.D2D1;

public partial class Foo
{
[D2DInputCount(2)]
[D2DInputSimple(0)]
[D2DInputComplex(1)]
[{|CMPSD2D0069:D2DCompileOptions(D2D1CompileOptions.Default | D2D1CompileOptions.EnableLinking)|}]
private partial struct MyShader : ID2D1PixelShader
{
public Float4 Execute() => 0;
}
}
""";

await CSharpAnalyzerWithLanguageVersionTest<InvalidD2D1CompileOptionsEnableLinkingOnShaderTypeAnalyzer>.VerifyAnalyzerAsync(source);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
extern alias Core;
#if D2D1_TESTS
extern alias D2D1;
#else
extern alias D3D12;
#endif

using System.Threading;
using System.Threading.Tasks;
Expand Down Expand Up @@ -48,7 +52,11 @@ public static Task VerifyAnalyzerAsync(string source, LanguageVersion languageVe

test.TestState.ReferenceAssemblies = ReferenceAssemblies.Net.Net80;
test.TestState.AdditionalReferences.Add(MetadataReference.CreateFromFile(typeof(Core::ComputeSharp.Hlsl).Assembly.Location));
#if D2D1_TESTS
test.TestState.AdditionalReferences.Add(MetadataReference.CreateFromFile(typeof(D2D1::ComputeSharp.D2D1.ID2D1PixelShader).Assembly.Location));
#else
test.TestState.AdditionalReferences.Add(MetadataReference.CreateFromFile(typeof(D3D12::ComputeSharp.IComputeShader).Assembly.Location));
#endif

return test.RunAsync(CancellationToken.None);
}
Expand Down