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 @@ -764,6 +764,7 @@ The Direct2D authoring projects were removed before the first release and restor
| 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` |
| Generated HLSL, a member of the shader hidden by a local | Drops the qualifier from a member of the shader when the access is written out, `this` from a field and the type name from a static field, without asking whether a local or a parameter of that name is in scope at the access. HLSL resolves a name by scope alone, so such a local hides the member there and the shader reads it in place of the member, compiling and computing a value C# never produces with nothing reporting it. A captured field read through `this` beside a local of its name, a static field read through the type beside one, and a captured field read in a method whose parameter has its name were each read back from a device as the local. A call to a method of the shader written through a qualifier, which the fork drops as the row for a call to a method of the shader through a qualifier records, is hidden the same way and fails in the shader compiler with a message naming generated code | Reports `CMPW0131`, or `CMPWD2D0104` on the Direct2D path, at the access as the author wrote it, from the type both rewriters derive from, so every site that drops a qualifier answers alike: the two field sites of the body rewriter, and the shared helper that drops the qualifier from a call to a method of the shader, which the body rewriter and the initializer rewriter both go through. What is asked is what the generated HLSL declares ahead of the access in the same function: a parameter, a local declared before the access, and a local declared in an argument, which the rewriting hoists ahead of the body. A local declared after the access is left alone, HLSL scoping it from its declaration, and so is a local of the enclosing method inside a local function, which is written out as a function of its own; a constant and a local function are written out under names of their own and hide nothing. The initializer rewriter drops the type name of a static field as well but an initializer declares no local and has no parameter, so that site is left as it is, and the helper finds nothing there for the same reason. Refusing was chosen over renaming the local: the author has two names in one function that C# tells apart and HLSL cannot, and a rename would decide in silence which one the generated code reads | `2fb7f200`, `946b6a87` |

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
4 changes: 2 additions & 2 deletions README.ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,7 @@ GPUが書いたバッファを、以降のシェーダーへ読み取り専用

### コンパイル時の検証

以上の宣言はアナライザーが検査し、接頭辞 `CMPW` の診断114種類として報告します。対象は属性の位置、ホストとパイプラインメソッドの形、スロットの宣言、資源の契約、生成されるオーバーロードの衝突です。一部にはコード修正が付きます。
以上の宣言はアナライザーが検査し、接頭辞 `CMPW` の診断115種類として報告します。対象は属性の位置、ホストとパイプラインメソッドの形、スロットの宣言、資源の契約、生成されるオーバーロードの衝突です。一部にはコード修正が付きます。

---

Expand Down Expand Up @@ -297,7 +297,7 @@ ReadOnlyMemory<byte> bytecode = D2D1PixelShader.LoadBytecode<DifferenceEffect>()
ReadOnlyMemory<byte> buffer = D2D1PixelShader.GetConstantBuffer(new DifferenceEffect(1));
```

これらの宣言は、`CMPWD2D` を接頭辞とする103件の診断を報告するアナライザーが検査します。シェーダーは Direct2D が受け付ける DXBC へ FXC でコンパイルします。`d3dcompiler_47.dll` は Windows に同梱されているため、このパッケージはコンパイラーを同梱しません。
これらの宣言は、`CMPWD2D` を接頭辞とする104件の診断を報告するアナライザーが検査します。シェーダーは Direct2D が受け付ける DXBC へ FXC でコンパイルします。`d3dcompiler_47.dll` は Windows に同梱されているため、このパッケージはコンパイラーを同梱しません。

---

Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,7 @@ The budget covers the resources the device creates itself. A device using an all

### Compile-time validation

The declarations above are checked by analyzers that report 114 diagnostics with the `CMPW` prefix, covering attribute placement, host and pipeline method shape, slot declaration, resource contracts and generated overload conflicts. Some carry a code fix.
The declarations above are checked by analyzers that report 115 diagnostics with the `CMPW` prefix, covering attribute placement, host and pipeline method shape, slot declaration, resource contracts and generated overload conflicts. Some carry a code fix.

---

Expand Down Expand Up @@ -297,7 +297,7 @@ ReadOnlyMemory<byte> bytecode = D2D1PixelShader.LoadBytecode<DifferenceEffect>()
ReadOnlyMemory<byte> buffer = D2D1PixelShader.GetConstantBuffer(new DifferenceEffect(1));
```

The declarations are checked by analyzers that report 103 diagnostics with the `CMPWD2D` prefix. Shaders are compiled to DXBC with FXC, which is what Direct2D accepts; `d3dcompiler_47.dll` ships with Windows, so the package bundles no compiler of its own.
The declarations are checked by analyzers that report 104 diagnostics with the `CMPWD2D` prefix. Shaders are compiled to DXBC with FXC, which is what Direct2D accepts; `d3dcompiler_47.dll` ships with Windows, so the package bundles no compiler of its own.

---

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ Rule ID | Category | Severity | Notes
CMPWD2D0101 | ComputeWeave.D2D1.Shaders | Error | [Documentation](https://github.com/routersys/ComputeWeave)
CMPWD2D0102 | ComputeWeave.D2D1.Shaders | Error | [Documentation](https://github.com/routersys/ComputeWeave)
CMPWD2D0103 | ComputeWeave.D2D1.Shaders | Error | [Documentation](https://github.com/routersys/ComputeWeave)
CMPWD2D0104 | ComputeWeave.D2D1.Shaders | Error | [Documentation](https://github.com/routersys/ComputeWeave)
Original file line number Diff line number Diff line change
Expand Up @@ -1593,4 +1593,24 @@ partial class DiagnosticDescriptors
isEnabledByDefault: true,
description: "The shared Hlsl type declares six memory barriers for the compute shaders, and a pixel shader is written with the same type. A pixel shader runs with no thread group to synchronize, so FXC refuses five of them under every pixel shader profile, while DeviceMemoryBarrier compiles under ps_4_0, ps_4_1 and ps_5_0 and is left alone; the two level 9 profiles refuse it as well, which the forwarded compiler error still reports, the rewriting not knowing the profile. Without this the call is written out as it stands and the shader compiler answers at the shader type rather than at the call. The compute generator refuses the intrinsics of the pixel stage the same way, so each of the two products refuses what its stage lacks.",
helpLinkUri: "https://github.com/routersys/ComputeWeave");

/// <summary>
/// Gets a <see cref="DiagnosticDescriptor"/> for a member of the shader accessed through a qualifier where a local or a parameter of its name hides it.
/// <para>
/// Format: <c>"The member {0} cannot be accessed through a qualifier here in a D2D1 pixel shader (the generated HLSL drops the qualifier, and a local variable or a parameter named '{1}' declared in the same function would be resolved instead)"</c>.
/// </para>
/// </summary>
/// <remarks>
/// This is the Direct2D counterpart of the compute diagnostic. The rewriting that drops the qualifier is
/// shared, so leaving one of the two out would refuse the same access on one path and not the other.
/// </remarks>
public static readonly DiagnosticDescriptor ShaderMemberHiddenByLocal = new(
id: "CMPWD2D0104",
title: "Shader member hidden by a local",
messageFormat: "The member {0} cannot be accessed through a qualifier here in a D2D1 pixel shader (the generated HLSL drops the qualifier, and a local variable or a parameter named '{1}' declared in the same function would be resolved instead)",
category: "ComputeWeave.D2D1.Shaders",
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true,
description: "A member of the shader is written out under its name alone, whatever qualifier the access carried, the generated HLSL declaring it at the top level: 'this' is dropped from a field, and the type name from a static field and from a method. HLSL resolves a name by scope alone, so a local or a parameter of that name in the function the access is written in hides the member there. Without this a field read through a qualifier compiles and reads the local, so the shader computes a value C# never produces with nothing reporting it, and a method called through one fails in the shader compiler naming generated code. What is refused is an access whose qualifier would be dropped where the generated HLSL declares a local or a parameter of the same name ahead of it in the same function: a parameter, a local declared before the access, and a local declared in an argument, which the rewriting hoists ahead of the body. A local declared after the access, one of an enclosing declaration, a constant and a local function are left alone, the first two being out of scope at the access in HLSL and the last two being written out under names of their own.",
helpLinkUri: "https://github.com/routersys/ComputeWeave");
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Linq;
using ComputeWeave.SourceGeneration.Extensions;
using ComputeWeave.SourceGeneration.Mappings;
using Microsoft.CodeAnalysis;
Expand Down Expand Up @@ -304,6 +305,62 @@ static bool IsInnermostWidening(IOperation operation)
}
}

/// <summary>
/// Reports a member of the shader accessed through a qualifier where a local or a parameter of its name hides it.
/// </summary>
/// <param name="node">The access as the author wrote it.</param>
/// <param name="member">The member of the shader the access resolves to.</param>
/// <remarks>
/// <para>
/// A member of the shader is written out under its name alone, whatever qualifier the access carried, the
/// generated HLSL declaring it at the top level: <see langword="this"/> is dropped from a field, and the type
/// name from a static field and from a method. HLSL resolves a name by scope alone, so a local or a parameter
/// of that name in the function the access is written in hides the member there: a field read through the
/// qualifier reads the local where C# read the field, with nothing reporting it, and a method called through
/// one fails in the shader compiler naming generated code.
/// </para>
/// <para>
/// What is asked is what the generated HLSL declares ahead of the access in the same function. A parameter
/// always is. A local declared after the access is not, HLSL scoping it from its declaration, so the access
/// reads the member there. One declared in an argument is written ahead of the whole body of the function
/// holding it by the rewriting that hoists it, so it counts wherever it was written, even in a block the
/// access is outside of. A local of an enclosing declaration is left out, a local function being written
/// out as a function of its own.
/// </para>
/// </remarks>
protected void ReportShaderMemberHiddenByLocal(SyntaxNode node, ISymbol member)
{
ISymbol? function = SemanticModel.For(node).GetEnclosingSymbol(node.SpanStart, CancellationToken);

foreach (ISymbol symbol in SemanticModel.For(node).LookupSymbols(node.SpanStart, name: member.Name))
{
if (!SymbolEqualityComparer.Default.Equals(symbol.ContainingSymbol, function))
{
continue;
}

if (symbol is IParameterSymbol ||
(symbol is ILocalSymbol local && local.DeclaringSyntaxReferences[0].Span.Start < node.SpanStart))
{
Diagnostics.Add(ShaderMemberHiddenByLocal, node, member, member.Name);

return;
}
}

// A declaration in an argument is out of scope where C# would not see it, so it is read from the syntax
// of the function rather than from the lookup, descending into no nested function, which hoists its own
SyntaxNode? body = node.Ancestors().FirstOrDefault(static ancestor => ancestor is LocalFunctionStatementSyntax or BaseMethodDeclarationSyntax);

if (body is not null &&
body.DescendantNodes(descendant => descendant == body || descendant is not LocalFunctionStatementSyntax)
.OfType<DeclarationExpressionSyntax>()
.Any(declaration => declaration.Designation is SingleVariableDesignationSyntax designation && designation.Identifier.ValueText == member.Name))
{
Diagnostics.Add(ShaderMemberHiddenByLocal, node, member, member.Name);
}
}

/// <inheritdoc/>
public sealed override SyntaxNode? VisitAnonymousObjectCreationExpression(AnonymousObjectCreationExpressionSyntax node)
{
Expand Down
Loading
Loading