Skip to content
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
1 change: 1 addition & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -763,6 +763,7 @@ The Direct2D authoring projects were removed before the first release and restor
| 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` |
| Generated HLSL, the intrinsics of the pixel stage in a compute shader | Maps the nine intrinsics of the pixel stage the shared `Hlsl` type declares (`Abort`, `Clip`, the six derivatives and `Fwidth`) onto their HLSL names in a compute shader the way it maps every intrinsic, and writes the call out. DXC refuses each of them under `cs_6_0`, and the failure lands on the shader type as the forwarded compiler error | Reports `CMPW0112` at the call, from the shared rewriter under the compute compilation symbol, naming the intrinsic and the reason DXC refuses it, so the compilation step is skipped and the author sees one report at their own line. The names and reasons are held by the compute partial of the known methods. The report is on the type the two rewriters share, because a derivative returns a value and so can be written into a static field initializer. This replaces the analyzer the fork had placed on the compute product on 2026-08-22, which read every invocation of the compilation whatever type held it: a project referencing the Direct2D product as well loaded it too and was refused a pixel shader using the clip or a derivative, which is legitimate there, and a compute shader was given that report and the forwarded compiler error together. The rewriting branch runs in the compute generator alone, so neither happens | `67b8c0b4` |

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 @@ -711,6 +711,15 @@ public override SyntaxNode VisitMemberAccessExpression(MemberAccessExpressionSyn
TrackKnownMethodInvocation(metadataName);

#if D3D12_SOURCE_GENERATOR
// Special case: the intrinsics of the pixel stage the shared Hlsl type declares are refused
// under cs_6_0, so the call is refused before the compiler is handed it. Direct2D shaders use
// them legitimately, so this is excluded from that generator by the compilation symbol, which
// is what keeps it away from a pixel shader in a project referencing both products.
if (ReportUnsupportedIntrinsic(node, metadataName, method))
{
return updatedNode;
}

// Special case: an intrinsic that writes through an out parameter terminates DXC on two
// shapes of matrix argument, so the call is refused before the compiler is handed it.
// Direct2D shaders are compiled with FXC, which does not have the defect, so this is
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,12 @@ public override SyntaxNode VisitMemberAccessExpression(MemberAccessExpressionSyn
TrackKnownMethodInvocation(metadataName);

#if D3D12_SOURCE_GENERATOR
// Refuse an intrinsic of the pixel stage (see ShaderSourceRewriter for more info)
if (ReportUnsupportedIntrinsic(node, metadataName, method))
{
return updatedNode;
}

// Refuse a matrix on an intrinsic with an out parameter (see ShaderSourceRewriter for more info)
if (ReportMatrixOnIntrinsicWithOutParameter(node, method))
{
Expand Down

This file was deleted.

32 changes: 32 additions & 0 deletions src/ComputeWeave.SourceGenerators/Mappings/HlslKnownMethods.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;

namespace ComputeWeave.SourceGeneration.Mappings;

Expand All @@ -24,6 +25,37 @@ public static bool SynchronizesTheWholeThreadGroup(string name)
"ComputeWeave.Hlsl.GroupMemoryBarrierWithGroupSync";
}

/// <summary>
/// Gets the reason a method name, previously matched with <see cref="TryGetMappedName(string, out string?)"/>,
/// cannot be used in a compute shader, if it cannot.
/// </summary>
/// <param name="name">The fully qualified metadata name.</param>
/// <param name="reason">The reason the intrinsic cannot be used, when it cannot.</param>
/// <returns>Whether the intrinsic cannot be used in a compute shader.</returns>
/// <remarks>
/// The shared <c>Hlsl</c> type declares intrinsics of the pixel stage as well, a pixel shader being written with
/// the same type. The nine here were measured to fail under <c>cs_6_0</c>: DXC does not accept the abort, a compute
/// shader has no pixel to discard, and the derivatives need shader model 6.6.
/// </remarks>
public static bool TryGetUnsupportedReason(string name, [NotNullWhen(true)] out string? reason)
{
reason = name switch
{
"ComputeWeave.Hlsl.Abort" => "the abort intrinsic is not accepted by the DXC compiler",
"ComputeWeave.Hlsl.Clip" => "the clip intrinsic discards a pixel, and a compute shader has no pixel to discard",
"ComputeWeave.Hlsl.DerivativeOfDx" or
"ComputeWeave.Hlsl.DerivativeOfDxHighPrecision" or
"ComputeWeave.Hlsl.DerivativeOfDxLowPrecision" or
"ComputeWeave.Hlsl.DerivativeOfDy" or
"ComputeWeave.Hlsl.DerivativeOfDyHighPrecision" or
"ComputeWeave.Hlsl.DerivativeOfDyLowPrecision" or
"ComputeWeave.Hlsl.Fwidth" => "derivatives in a compute shader require shader model 6.6, while shaders are compiled as cs_6_0",
_ => null
};

return reason is not null;
}

/// <inheritdoc/>
private static partial Dictionary<string, string?> BuildKnownResourceSamplers()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,40 @@ protected partial void TrackKnownMethodInvocation(string metadataName)
Requirements.SynchronizesTheWholeThreadGroup |= HlslKnownMethods.SynchronizesTheWholeThreadGroup(metadataName);
}

/// <summary>
/// Reports an intrinsic being invoked that a compute shader cannot use.
/// </summary>
/// <param name="node">The invocation that is about to be written out as it stands.</param>
/// <param name="metadataName">The fully qualified metadata name of the intrinsic being invoked.</param>
/// <param name="method">The resolved target of <paramref name="node"/>.</param>
/// <returns>Whether the invocation was reported, in which case the caller leaves it alone.</returns>
/// <remarks>
/// <para>
/// The shared <c>Hlsl</c> type declares the intrinsics of the pixel stage as well, and DXC refuses each of them
/// under <c>cs_6_0</c> for a reason of its own, which the report names. Reporting here keeps the call from reaching
/// the compiler at all, the compilation step being skipped for a shader whose rewriting produced an error, so the
/// author sees one report at their own line rather than that report and the forwarded compiler error together.
/// </para>
/// <para>
/// This was an analyzer before, reading every invocation of the compilation whatever type held it. A project
/// referencing the Direct2D product as well loads that analyzer too, and a pixel shader there uses the clip and
/// the derivatives legitimately, so the refusal is on the rewriting the compute generator alone runs. The report
/// is on the type the two rewriters share, because a derivative returns a value and so can be written into a
/// static field initializer as well as into a body.
/// </para>
/// </remarks>
protected bool ReportUnsupportedIntrinsic(InvocationExpressionSyntax node, string metadataName, IMethodSymbol method)
{
if (!HlslKnownMethods.TryGetUnsupportedReason(metadataName, out string? reason))
{
return false;
}

Diagnostics.Add(UnsupportedHlslIntrinsicInvocation, node, method.Name, reason);

return true;
}

/// <summary>
/// Reports an intrinsic that writes through an out parameter being given a matrix the compiler terminates on.
/// </summary>
Expand Down
19 changes: 9 additions & 10 deletions tests/ComputeWeave.D2D1.Tests/ComputeOnlyIntrinsicsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ namespace ComputeWeave.D2D1.Tests;
/// </summary>
/// <remarks>
/// <para>
/// <c>UnsupportedHlslIntrinsicAnalyzer</c> rejects these nine with <c>CMPW0112</c>. It lives in
/// <c>ComputeWeave.SourceGenerators</c>, which no D2D1 consumer loads, so the rejection does not
/// reach the authoring path. A pixel shader has pixels to discard and a stage that computes
/// derivatives, so eight of the nine are ordinary here.
/// The compute generator rejects these nine with <c>CMPW0112</c>, from the rewriting it alone runs,
/// so the rejection does not reach the authoring path even in a project referencing both products.
/// A pixel shader has pixels to discard and a stage that computes derivatives, so eight of the nine
/// are ordinary here.
/// </para>
/// <para>
/// <c>Abort</c> is the exception, and not for the compute-side reason. D2D1 compiles a shader twice
Expand All @@ -26,12 +26,11 @@ namespace ComputeWeave.D2D1.Tests;
/// the tests below pin both halves against controls that isolate the instruction from the options.
/// </para>
/// <para>
/// That this file compiles at all is half the measurement: this project loads
/// <c>ComputeWeave.D2D1.SourceGenerators</c> and <c>ComputeWeave.Core.SourceGenerators</c> as
/// analyzers, and a <c>CMPW0112</c> reaching here would be an error. The other half is the
/// compute-side counterpart, <c>UnsupportedHlslIntrinsicAnalyzerTests</c>, which asserts that the
/// same nine names do produce <c>CMPW0112</c>. Without it, this test would also pass if the
/// analyzer had simply stopped working.
/// That this file compiles and its effects draw is half the measurement: the eight are usable here.
/// The other half is the compute-side counterpart, <c>UnsupportedIntrinsicTests</c>, which asserts
/// that the same nine names do produce <c>CMPW0112</c> in a compute shader, and that a type which is
/// no compute shader is left alone. Without it, this test would also pass if the refusal had simply
/// stopped working.
/// </para>
/// </remarks>
[TestClass]
Expand Down
Loading
Loading