From 3210e52fcc5afb6105aa91c96b3daf8d3a3fcb20 Mon Sep 17 00:00:00 2001 From: routersys Date: Tue, 15 Sep 2026 16:23:49 +0900 Subject: [PATCH 1/3] =?UTF-8?q?=E3=82=B7=E3=82=A7=E3=83=BC=E3=83=80?= =?UTF-8?q?=E3=83=BC=E8=87=AA=E8=BA=AB=E3=81=AE=E3=83=A1=E3=82=BD=E3=83=83?= =?UTF-8?q?=E3=83=89=E3=81=AE=E9=99=90=E5=AE=9A=E5=AD=90=E4=BB=98=E3=81=8D?= =?UTF-8?q?=E3=81=AE=E5=91=BC=E3=81=B3=E5=87=BA=E3=81=97=E3=82=92=E5=90=8D?= =?UTF-8?q?=E5=89=8D=E3=81=A0=E3=81=91=E3=81=A7=E6=9B=B8=E3=81=8D=E5=87=BA?= =?UTF-8?q?=E3=81=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../SyntaxRewriters/HlslSourceRewriter.cs | 21 +++++++++++++++++++ .../SyntaxRewriters/ShaderSourceRewriter.cs | 10 +++++++++ .../SyntaxRewriters/StaticFieldRewriter.cs | 5 ++++- 3 files changed, 35 insertions(+), 1 deletion(-) diff --git a/src/ComputeWeave.SourceGeneration.Hlsl/SyntaxRewriters/HlslSourceRewriter.cs b/src/ComputeWeave.SourceGeneration.Hlsl/SyntaxRewriters/HlslSourceRewriter.cs index d76cd9306..ace89d57b 100644 --- a/src/ComputeWeave.SourceGeneration.Hlsl/SyntaxRewriters/HlslSourceRewriter.cs +++ b/src/ComputeWeave.SourceGeneration.Hlsl/SyntaxRewriters/HlslSourceRewriter.cs @@ -789,6 +789,27 @@ protected InvocationExpressionSyntax VisitConvertedIntrinsicArguments( } } + /// + /// Rewrites a call to a method of the shader type to name the method alone. + /// + /// The updated instance with tweaked syntax. + /// The invocation, with the qualifier the call was written through dropped. + /// + /// Both generators write the methods of the shader type out at the top level under their own names, so a + /// call qualified with the shader type, with an alias of it or with names something + /// the generated HLSL never declares. The name is kept as visited rather than read from the symbol, so a + /// method named after an HLSL keyword stays mapped the way its declaration is. + /// + protected static InvocationExpressionSyntax VisitShaderMethodInvocation(InvocationExpressionSyntax updatedNode) + { + if (updatedNode.Expression is MemberAccessExpressionSyntax qualifiedName) + { + return updatedNode.WithExpression(qualifiedName.Name); + } + + return updatedNode; + } + /// /// Raises the requirements a call to a known HLSL method places on the shader, if it places any. /// diff --git a/src/ComputeWeave.SourceGeneration.Hlsl/SyntaxRewriters/ShaderSourceRewriter.cs b/src/ComputeWeave.SourceGeneration.Hlsl/SyntaxRewriters/ShaderSourceRewriter.cs index fd9d6e597..9a231e7a7 100644 --- a/src/ComputeWeave.SourceGeneration.Hlsl/SyntaxRewriters/ShaderSourceRewriter.cs +++ b/src/ComputeWeave.SourceGeneration.Hlsl/SyntaxRewriters/ShaderSourceRewriter.cs @@ -776,6 +776,9 @@ public override SyntaxNode VisitMemberAccessExpression(MemberAccessExpressionSyn // A static method of the shader is left for the generator's own path, so a cycle an // initializer closes through one is answered by walking it (see HlslSourceRewriter) ReportCyclicStaticFieldInitializerThroughShaderMethod(method); + + // The call is written the way the generator writes the method out, under its name alone + return VisitShaderMethodInvocation(updatedNode); } else { @@ -825,6 +828,13 @@ public override SyntaxNode VisitMemberAccessExpression(MemberAccessExpressionSyn return updatedNode; } + + // An instance method of the shader is written out the way a static one is, so a call + // through 'this' is renamed the same way + if (SymbolEqualityComparer.Default.Equals(ShaderType, method.ContainingType)) + { + return VisitShaderMethodInvocation(updatedNode); + } } } diff --git a/src/ComputeWeave.SourceGeneration.Hlsl/SyntaxRewriters/StaticFieldRewriter.cs b/src/ComputeWeave.SourceGeneration.Hlsl/SyntaxRewriters/StaticFieldRewriter.cs index 83ee632c9..2378ba587 100644 --- a/src/ComputeWeave.SourceGeneration.Hlsl/SyntaxRewriters/StaticFieldRewriter.cs +++ b/src/ComputeWeave.SourceGeneration.Hlsl/SyntaxRewriters/StaticFieldRewriter.cs @@ -256,7 +256,7 @@ public override SyntaxNode VisitMemberAccessExpression(MemberAccessExpressionSyn // A static method with no mapping is imported by rewriting its declaration, the same way the // shader body imports one. HLSL accepts a call in a static field initializer because every // forward declaration is written ahead of the static fields. A method on the shader type is - // left alone for the generator to write out, so what it reaches is walked for a cycle instead. + // written out by the generator itself, so what it reaches is walked for a cycle instead. if (method.IsStatic) { if (!SymbolEqualityComparer.Default.Equals(ShaderType, method.ContainingType)) @@ -265,6 +265,9 @@ public override SyntaxNode VisitMemberAccessExpression(MemberAccessExpressionSyn } ReportCyclicStaticFieldInitializerThroughShaderMethod(method); + + // The call is written the way the generator writes the method out, under its name alone + return VisitShaderMethodInvocation(updatedNode); } } From 14b3d946d805119e0ca18fe32a84835cded008b6 Mon Sep 17 00:00:00 2001 From: routersys Date: Tue, 15 Sep 2026 16:23:50 +0900 Subject: [PATCH 2/3] =?UTF-8?q?=E9=99=90=E5=AE=9A=E5=AD=90=E4=BB=98?= =?UTF-8?q?=E3=81=8D=E3=81=AE=E5=91=BC=E3=81=B3=E5=87=BA=E3=81=97=E3=81=AE?= =?UTF-8?q?=E8=A9=A6=E9=A8=93=E3=82=92=E4=B8=A1=E7=94=9F=E6=88=90=E5=99=A8?= =?UTF-8?q?=E3=81=B8=E8=B6=B3=E3=81=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...elShaderDescriptorGenerator_Diagnostics.cs | 94 ++++++++ .../Shaders/QualifiedShaderMethodCallTests.cs | 214 ++++++++++++++++++ 2 files changed, 308 insertions(+) create mode 100644 tests/ComputeWeave.Tests.SourceGenerators/Shaders/QualifiedShaderMethodCallTests.cs diff --git a/tests/ComputeWeave.D2D1.Tests.SourceGenerators/Test_D2DPixelShaderDescriptorGenerator_Diagnostics.cs b/tests/ComputeWeave.D2D1.Tests.SourceGenerators/Test_D2DPixelShaderDescriptorGenerator_Diagnostics.cs index 263ea9317..ae2069201 100644 --- a/tests/ComputeWeave.D2D1.Tests.SourceGenerators/Test_D2DPixelShaderDescriptorGenerator_Diagnostics.cs +++ b/tests/ComputeWeave.D2D1.Tests.SourceGenerators/Test_D2DPixelShaderDescriptorGenerator_Diagnostics.cs @@ -421,6 +421,100 @@ public float4 Execute() CSharpGeneratorTest.VerifyDiagnostics(source); } + /// + /// A static method of the shader called through the shader type. The rename to the name alone is shared + /// with the compute generator, so what this pins is that the pixel shader path reaches it from the body. + /// + [TestMethod] + public void AShaderMethodCalledThroughTheShaderTypeIsAccepted() + { + const string source = """ + using ComputeWeave; + using ComputeWeave.D2D1; + using float4 = global::ComputeWeave.Float4; + + namespace MyNamespace; + + [D2DInputCount(0)] + [D2DShaderProfile(D2D1ShaderProfile.PixelShader50)] + [D2DGeneratedPixelShaderDescriptor] + internal readonly partial struct MyShader : ID2D1PixelShader + { + private static float Twice() => 2.0f; + + public float4 Execute() + { + return new float4(MyShader.Twice(), 0, 0, 1); + } + } + """; + + CSharpGeneratorTest.VerifyDiagnostics(source); + } + + /// + /// The same call in a static field initializer, which the rewriter for initializers answers. + /// + [TestMethod] + public void AShaderMethodCalledThroughTheShaderTypeInAnInitializerIsAccepted() + { + const string source = """ + using ComputeWeave; + using ComputeWeave.D2D1; + using float4 = global::ComputeWeave.Float4; + + namespace MyNamespace; + + [D2DInputCount(0)] + [D2DShaderProfile(D2D1ShaderProfile.PixelShader50)] + [D2DGeneratedPixelShaderDescriptor] + internal readonly partial struct MyShader : ID2D1PixelShader + { + private static readonly float Scale = MyShader.Twice(); + + private static float Twice() => 2.0f; + + public float4 Execute() + { + return new float4(Scale, 0, 0, 1); + } + } + """; + + CSharpGeneratorTest.VerifyDiagnostics(source); + } + + /// + /// An instance method of the shader called through , which the generated HLSL has + /// no counterpart for either. + /// + [TestMethod] + public void AShaderMethodCalledThroughThisIsAccepted() + { + const string source = """ + using ComputeWeave; + using ComputeWeave.D2D1; + using float4 = global::ComputeWeave.Float4; + + namespace MyNamespace; + + [D2DInputCount(0)] + [D2DShaderProfile(D2D1ShaderProfile.PixelShader50)] + [D2DGeneratedPixelShaderDescriptor] + internal readonly partial struct MyShader : ID2D1PixelShader + { + private float Twice() => 2.0f; + + public float4 Execute() + { + return new float4(this.Twice(), 0, 0, 1); + } + } + """; + + CSharpGeneratorTest.VerifyDiagnostics(source); + } + /// /// A type declaring a primary constructor. The rewriters are shared with the compute generator, so what /// this pins is that the pixel shader generator answers with its own identifier. diff --git a/tests/ComputeWeave.Tests.SourceGenerators/Shaders/QualifiedShaderMethodCallTests.cs b/tests/ComputeWeave.Tests.SourceGenerators/Shaders/QualifiedShaderMethodCallTests.cs new file mode 100644 index 000000000..0f6d88c25 --- /dev/null +++ b/tests/ComputeWeave.Tests.SourceGenerators/Shaders/QualifiedShaderMethodCallTests.cs @@ -0,0 +1,214 @@ +using System.Collections.Immutable; +using System.Linq; +using ComputeWeave.SourceGenerators; +using ComputeWeave.Tests.SourceGenerators.Helpers; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace ComputeWeave.Tests.SourceGenerators.Shaders; + +/// +/// A call to a method of the shader type, written through a qualifier. The generator writes those methods +/// out at the top level under their own names, so the qualifier names nothing in the generated HLSL, and a +/// call written through one has to be written the way the same call written by name alone is. +/// +/// +/// The shaders here carry a thread group size, which is what turns shader compilation on, so a qualifier +/// that reaches the generated HLSL arrives as a diagnostic rather than as a string the assertions could miss. +/// +[TestClass] +public class QualifiedShaderMethodCallTests +{ + /// + /// A static method called through the shader type, under each spelling the type can be named by. The + /// alias is declared for the row that calls through it. + /// + [TestMethod] + [DataRow("Shader.Twice()", "QualifiedCallTypeNameTests")] + [DataRow("Shaders.Shader.Twice()", "QualifiedCallNamespaceTests")] + [DataRow("global::Shaders.Shader.Twice()", "QualifiedCallGlobalTests")] + [DataRow("S.Twice()", "QualifiedCallAliasTests")] + public void AStaticMethodCalledThroughTheShaderTypeIsNamedDirectly(string call, string assemblyName) + { + string source = $$""" + using ComputeWeave; + using S = Shaders.Shader; + + namespace Shaders; + + [ThreadGroupSize(DefaultThreadGroupSizes.X)] + [GeneratedComputeShaderDescriptor] + internal readonly partial struct Shader : IComputeShader + { + private readonly ReadWriteBuffer buffer; + + private static float Twice() => 2.0f; + + public void Execute() + { + this.buffer[0] = {{call}}; + } + } + """; + + AssertNamedDirectly(source, assemblyName, "= Twice()"); + } + + /// + /// The same call in a static field initializer, which a rewriter of its own handles. + /// + [TestMethod] + public void AStaticMethodCalledThroughTheShaderTypeInAnInitializerIsNamedDirectly() + { + const string Source = """ + using ComputeWeave; + + namespace Shaders; + + [ThreadGroupSize(DefaultThreadGroupSizes.X)] + [GeneratedComputeShaderDescriptor] + internal readonly partial struct Shader : IComputeShader + { + private static readonly float Scale = Shader.Twice(); + + private readonly ReadWriteBuffer buffer; + + private static float Twice() => 2.0f; + + public void Execute() + { + this.buffer[0] = Scale; + } + } + """; + + AssertNamedDirectly(Source, "QualifiedCallInitializerTests", "= Twice()"); + } + + /// + /// The same call from a method declared outside the shader, where the type name is the only way to + /// write it. The declaration is imported by a rewriter created for it. + /// + [TestMethod] + public void AStaticMethodCalledThroughTheShaderTypeFromAnImportedMethodIsNamedDirectly() + { + const string Source = """ + using ComputeWeave; + + namespace Shaders; + + internal static class Helper + { + public static float Go() => Shader.Twice(); + } + + [ThreadGroupSize(DefaultThreadGroupSizes.X)] + [GeneratedComputeShaderDescriptor] + internal readonly partial struct Shader : IComputeShader + { + private readonly ReadWriteBuffer buffer; + + internal static float Twice() => 2.0f; + + public void Execute() + { + this.buffer[0] = Helper.Go(); + } + } + """; + + AssertNamedDirectly(Source, "QualifiedCallImportedMethodTests", "return Twice()"); + } + + /// + /// An instance method called through . The generator writes it out the way it + /// writes a static one, so the qualifier names nothing there either. + /// + [TestMethod] + public void AnInstanceMethodCalledThroughThisIsNamedDirectly() + { + const string Source = """ + using ComputeWeave; + + namespace Shaders; + + [ThreadGroupSize(DefaultThreadGroupSizes.X)] + [GeneratedComputeShaderDescriptor] + internal readonly partial struct Shader : IComputeShader + { + private readonly ReadWriteBuffer buffer; + + private float Twice() => 2.0f; + + public void Execute() + { + this.buffer[0] = this.Twice(); + } + } + """; + + AssertNamedDirectly(Source, "QualifiedCallThisTests", "= Twice()"); + } + + /// + /// A method named after an HLSL keyword, called through the shader type. Its declaration is written + /// under the mapped name, so the call has to keep the mapping rather than the name the author wrote. + /// + [TestMethod] + public void AMethodNamedAfterAKeywordCalledThroughTheShaderTypeKeepsItsMappedName() + { + const string Source = """ + using ComputeWeave; + + namespace Shaders; + + [ThreadGroupSize(DefaultThreadGroupSizes.X)] + [GeneratedComputeShaderDescriptor] + internal readonly partial struct Shader : IComputeShader + { + private readonly ReadWriteBuffer buffer; + + private static float sample() => 2.0f; + + public void Execute() + { + this.buffer[0] = Shader.sample(); + } + } + """; + + AssertNamedDirectly(Source, "QualifiedCallKeywordTests", "= __reserved__sample()"); + } + + /// + /// Asserts that the generated HLSL compiles and calls a method of the shader by its name alone. + /// + /// The shader source to generate from. + /// The assembly name to compile under. + /// The call as it has to be written, with what precedes it on the line. + private static void AssertNamedDirectly(string source, string assemblyName, string written) + { + CSharpCompilation compilation = CompilationHelper.CreateCompilation( + [source], + assemblyName, + new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, allowUnsafe: true)); + GeneratorDriver driver = GeneratorHelper.CreateDriver(new ComputeShaderDescriptorGenerator()); + GeneratorRunResult result = driver.RunGenerators(compilation).GetRunResult().Results[0]; + + Assert.IsNull(result.Exception, result.Exception?.ToString()); + + ImmutableArray diagnostics = result.Diagnostics; + + // A qualifier left in the generated HLSL is what the shader compiler reports, so this is read first + Assert.IsTrue( + diagnostics.IsEmpty, + string.Join(", ", diagnostics.Select(static diagnostic => diagnostic.ToString()))); + + string generated = GeneratorHelper.GetGeneratedSource(result.GeneratedSources, "Shaders.Shader"); + string name = written[(written.LastIndexOf(' ') + 1)..]; + + Assert.IsTrue(generated.Contains(written), $"the call is not written by name alone:\n{generated}"); + Assert.IsFalse(generated.Contains($".{name}"), $"the qualifier is written out:\n{generated}"); + } +} From 71d44e92defe9ee25ba0e7d5045e1b3d8dea387b Mon Sep 17 00:00:00 2001 From: routersys Date: Tue, 15 Sep 2026 17:33:53 +0900 Subject: [PATCH 3/3] =?UTF-8?q?=E4=B9=96=E9=9B=A2=E5=8F=B0=E5=B8=B3?= =?UTF-8?q?=E3=81=B8=E9=99=90=E5=AE=9A=E5=AD=90=E4=BB=98=E3=81=8D=E3=81=AE?= =?UTF-8?q?=E5=91=BC=E3=81=B3=E5=87=BA=E3=81=97=E3=81=AE=E8=A1=8C=E3=82=92?= =?UTF-8?q?=E8=B6=B3=E3=81=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CONTRIBUTING.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c3e36be4e..b2ae4a1e3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -756,6 +756,7 @@ The Direct2D authoring projects were removed before the first release and restor | Generated HLSL, custom type declarations | Orders the custom type definitions by their fields alone, forward declares every custom type ahead of them on the compute path, and forward declares none on the Direct2D path. DXC accepts the forward declarations, so the shipped path is unaffected, but FXC has no type forward declaration and refuses each one with `X3000`, so a caller who takes the generated HLSL of a shader using a custom type and builds it for Direct3D 11 has to delete those lines by hand. The order by fields alone leaves a member method prototype free to name a type defined after it, which the forward declarations mask on the compute path, while on the Direct2D path such a prototype reaches FXC naming an undeclared type, with nothing that names the cause at the declaration. The order is found by dequeuing a type until none of its fields waits on another, which never comes for a type in a struct layout cycle, so on source C# has already refused with `CS0523` the generator never returns, and no cancellation reaches the loop | Orders the definitions by the fields and the member method prototypes each of them holds, so a prototype naming another type finds it declared, and forward declares a type only where the prototypes name each other in a cycle that no order resolves. Such a cycle cannot run through fields, which are refused first: a type reached again through its own fields is reported as an invalid discovered type, with every other type on that cycle, because the shader compiler runs out of stack on a use of such a type rather than reporting it, and the refusal is what keeps its bytecode from being compiled. The type written first is then the first in line whose fields are declared, and the types its prototypes still name are the ones forward declared. The Direct2D generator, whose compiler has no such declaration, reports `CMPWD2D0100` at each prototype naming such a type instead of writing HLSL that cannot build, that prototype being the place to change. Of the 22 generated compute shaders in this repository that carried forward declarations, 21 lose them all and compile to byte identical DXIL under `cs_6_0`, and they build under FXC at `cs_5_0` as a result; the one whose two custom types name each other keeps the single forward declaration it needs, which DXC accepts and FXC cannot, that shape having no spelling FXC takes | `e00057fb`, `6ac88e40` | | Constant buffer layout, a captured struct in a layout cycle | Walks the captured fields into every nested unmanaged struct to lay the constant buffer out, with nothing that notices a type it is already inside. A struct whose fields lead back to itself, which C# refuses with `CS0523` but which the generator and the dispatch data size analyzers run over all the same while the author is in the middle of writing it, is followed around the cycle until the stack runs out, and the process hosting the generator ends with it | Skips a field whose type is already being walked, in the walk that lays the fields out and in the one that only sizes them alike, so the walk ends. The type is then refused by the discovered type check as one HLSL cannot lay out, which reports `CMPW0050` or `CMPWD2D0041` and keeps the shader compiler from being run over it; the C# compiler already names the field, so nothing further is reported here | `245fe8ab` | | Custom type discovery, a field of a type with no name | Casts the type of every field of a discovered custom type to a named type while exploring the nested types. A pointer field, or a fixed size buffer whose field type is a pointer as well, is valid C# in an unsafe struct and ends the generator with `InvalidCastException`, which Roslyn reports as `CS8785` while discarding the descriptors of every shader in the compilation unit | Refuses a custom type holding such a field as an invalid discovered type, reporting `CMPW0050` or `CMPWD2D0041` for the type the way a `bool` field is reported, and stops exploring it. HLSL has no pointer, and while a struct in HLSL does hold a fixed size array, the generator writes no mapping for a fixed size buffer, neither the field itself nor the constant buffer layout an array in one would need, so there is nothing to write for either as things stand; a shader whose own field is one of these was already refused by the field check, and stays so | `c8cc970f` | +| Generated HLSL, a call to a method of the shader through a qualifier | Leaves a static method of the shader for the generator to write out and writes the call to it as it stands, so a call qualified with the shader type, with an alias of it, with its namespace or with `global::` keeps the qualifier in the generated HLSL, where nothing declares it. An instance method called through `this` answers the same way, `this` being dropped from a field access and from nothing else. The same call written by name alone compiles, so how the author spells it decides whether the shader does, and what it fails with is the shader compiler naming a line of generated HLSL rather than the call the author wrote. A method declared outside the shader has no other way to call one of the shader's static methods | Writes the call under the name alone, whatever the qualifier, through one helper on the type both rewriters derive from, so the shader body, a static field initializer and every declaration either of them imports answer alike, and the static and the instance paths of the body rewriter reach the same rename. The name is kept as visited rather than read from the symbol: an identifier that collides with an HLSL keyword is mapped as it is visited and the declaration is written under the mapped name, so reading the symbol would leave that one spelling uncompilable, which a test pins and a mutation writing the symbol's name was measured to fail. Measured over fifteen shapes on the compute path and ten on the Direct2D one, every qualified call failing in the shader compiler before the change and compiling after it, and every unqualified one writing the same HLSL either way. The generated sources of the whole solution are unchanged across the change, no shader in the tree writing such a call, which before the change none could | `3210e52f` | The ledger is kept current by derivation, not by memory: `build/audit-upstream-divergence.ps1` lists every commit after the marker below that modifies an inherited `.cs` file under `src/` and is not cited by a row. That is the whole of what it reads. Inherited project files, documents and test sources are outside it, so a divergence in one of those reaches the ledger only because someone wrote the row by hand. Run it before a release. When the queue is not empty, either add a row or decide that the commit is not a divergence, and then move the marker. The ledger is audited through commit `c8cc970f`.