Skip to content
Merged
1 change: 1 addition & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -761,6 +761,7 @@ The Direct2D authoring projects were removed before the first release and restor
| Generated HLSL, native integer constants | Writes a `nint` or a `nuint` constant as its digits, the value being boxed as a 32 bit one, so the shader computes with it at 32 bits while C# computes with it at the width of the platform: a result past 32 bits wraps, and the refusal of a widened operand reads the left operand alone, so the constant on that side escapes it as a 64 bit one does | Refuses a native integer constant by tracking its type, the way a local of that type and a 64 bit constant already are, so the shader is reported with `CMPW0050`, or `CMPWD2D0041` on the Direct2D path. The one function that decides which constants have an HLSL literal takes the type of the constant as well as its value, since the value cannot tell a native integer apart. No shader in this repository carries such a constant, so the generated files are unchanged | `e33998ba` |
| Generated HLSL, a call leading back to the declaration it is written in | Writes every call out as it stands and imports a declaration once, claiming its entry before the rewriting so that a method reaching itself ends the import rather than the stack, and nothing reads what that claim just found. HLSL has no recursion, so the shader compiler refuses the function, and the failure lands on the shader type as the forwarded compiler error, naming a function of the generated HLSL, which for a local function or a method of a custom type is a name the generator gave it. Six shapes were measured on the compute path and two on the Direct2D path, each accepted by the generator and refused by the shader compiler | Records every call the generated HLSL holds as it is rewritten, from the declaration being rewritten to the one the call resolves to, into one collection every rewriter for a shader shares the way the requirements are shared, and reads it once every declaration is rewritten for the calls whose target reaches back to the declaration holding them. Each is reported as `CMPW0129`, or `CMPWD2D0101` on the Direct2D path, at the call as the author wrote it, naming the callee and the declaration in C# terms. The read comes after the rewriting rather than during it because no rewriting passes through every edge of a cycle: a method of the shader is written out by the generator's own path rather than imported, a local function is lifted where it is declared, and an imported declaration is rewritten once. Every call on a cycle is reported rather than the one a traversal happens to close it on, so removing any of them resolves the report and the set does not depend on the order the declarations were reached in. A partial declaration is recorded under its defining part, which is what a call to it resolves to; a call to a declaration with no source is left to the report it already had; a static field initializer is not a function and is not a node, while the declarations it imports are recorded by the rewriter importing them | `68ebbaed` |
| Generated HLSL, a thread synchronization in a pixel shader | Maps the six memory barriers the shared `Hlsl` type declares for the compute shaders onto their HLSL names on the Direct2D path the way it maps every intrinsic, and writes the call out. A pixel shader runs with no thread group to synchronize, so FXC refuses five of the six under every pixel shader profile with `X3664`, and the failure lands on the shader type as the forwarded compiler error, naming the intrinsic but not the call. The compute path refuses the intrinsics of the pixel stage through an analyzer, so the two products did not mirror each other | Reports `CMPWD2D0102` at the call, from the shared rewriter under the Direct2D compilation symbol, the way the compute path refuses a matrix given to an intrinsic with an out parameter under its own symbol. The names are held by the Direct2D partial of the known methods, with `DeviceMemoryBarrier` left out because FXC accepts it, which was measured one name at a time. The report is on the shader rewriter alone: a barrier returns nothing, so no static field initializer can hold one, and a method an initializer imports is rewritten by the shader rewriter like any other. An analyzer was not chosen, because one would flag a compute shader in a project referencing both products, whereas the rewriter refuses only what the Direct2D generator writes out | `c45fe471`, `e982d57d` |
| 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`.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -843,6 +843,27 @@ protected InvocationExpressionSyntax VisitConvertedIntrinsicArguments(
}
}

/// <summary>
/// Rewrites a call to a method of the shader type to name the method alone.
/// </summary>
/// <param name="updatedNode">The updated <see cref="InvocationExpressionSyntax"/> instance with tweaked syntax.</param>
/// <returns>The invocation, with the qualifier the call was written through dropped.</returns>
/// <remarks>
/// 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 <see langword="this"/> 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.
/// </remarks>
protected static InvocationExpressionSyntax VisitShaderMethodInvocation(InvocationExpressionSyntax updatedNode)
{
if (updatedNode.Expression is MemberAccessExpressionSyntax qualifiedName)
{
return updatedNode.WithExpression(qualifiedName.Name);
}

return updatedNode;
}

/// <summary>
/// Raises the requirements a call to a known HLSL method places on the shader, if it places any.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -820,6 +820,10 @@ public override SyntaxNode VisitMemberAccessExpression(MemberAccessExpressionSyn

return updatedNode.WithExpression(IdentifierName(methodIdentifier));
}

// A static method of the shader is written out by the generator itself, so the call is
// written the way it writes the method out, under its name alone
return VisitShaderMethodInvocation(updatedNode);
}
else
{
Expand Down Expand Up @@ -869,6 +873,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);
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -248,11 +248,15 @@ 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, as the generator writes those out through its own path.
if (method.IsStatic &&
!SymbolEqualityComparer.Default.Equals(ShaderType, method.ContainingType))
// written out by the generator itself, so the call is written the way it writes the method out
if (method.IsStatic)
{
return VisitImportedStaticMethodInvocation(node, updatedNode, method);
if (!SymbolEqualityComparer.Default.Equals(ShaderType, method.ContainingType))
{
return VisitImportedStaticMethodInvocation(node, updatedNode, method);
}

return VisitShaderMethodInvocation(updatedNode);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -528,6 +528,100 @@ public float4 Execute()
CSharpGeneratorTest<D2DPixelShaderDescriptorGenerator>.VerifyDiagnostics(source);
}

/// <summary>
/// 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.
/// </summary>
[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<D2DPixelShaderDescriptorGenerator>.VerifyDiagnostics(source);
}

/// <summary>
/// The same call in a static field initializer, which the rewriter for initializers answers.
/// </summary>
[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<D2DPixelShaderDescriptorGenerator>.VerifyDiagnostics(source);
}

/// <summary>
/// An instance method of the shader called through <see langword="this"/>, which the generated HLSL has
/// no counterpart for either.
/// </summary>
[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<D2DPixelShaderDescriptorGenerator>.VerifyDiagnostics(source);
}

/// <summary>
/// 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.
Expand Down
Loading
Loading