Skip to content
1 change: 1 addition & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -760,6 +760,7 @@ The Direct2D authoring projects were removed before the first release and restor
| Generated HLSL, 64 bit integer literals and constants | Writes a `long` or a `ulong` literal with the suffix the author wrote and a constant of either type as its digits, and leaves a `string` constant as an identifier. The shader compiler reads the digits as a 32 bit value, so a negative literal on the left of a comparison with an unsigned value is compared as unsigned, and a value past the 32 bit range is truncated, with no diagnostic from either compiler; the refusal of a widened operand reads the left operand alone, which a literal is not a conversion of, so a 64 bit literal on that side escapes it as well | Refuses a literal or a constant whose type the generator does not map by tracking that type, so the shader is reported with `CMPW0050`, or `CMPWD2D0041` on the Direct2D path, the way a local of that type already is. The three sites that turn a constant into an HLSL definition share one helper whose failure path is that refusal, so a string constant is refused the same way rather than left as an identifier the shader compiler cannot resolve. No shader in this repository carries such a literal or constant, so the generated files are unchanged, which was compared in full | `1c3c0ecf`, `8c5db0ab` |
| 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` |

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
3 changes: 2 additions & 1 deletion README.ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -297,7 +297,7 @@ ReadOnlyMemory<byte> bytecode = D2D1PixelShader.LoadBytecode<DifferenceEffect>()
ReadOnlyMemory<byte> buffer = D2D1PixelShader.GetConstantBuffer(new DifferenceEffect(1));
```

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

---

Expand Down Expand Up @@ -477,6 +477,7 @@ ReadOnlyMemory<byte> buffer = D2D1PixelShader.GetConstantBuffer(new DifferenceEf
- `AllMemoryBarrierWithGroupSync`、`DeviceMemoryBarrierWithGroupSync`、`GroupMemoryBarrierWithGroupSync` のいずれかへ到達するシェーダーは、各軸の差し渡しが群の大きさの倍数である場合にだけ派遣できます。派遣は差し渡しを群へ切り上げ、入口は要求された範囲の中にあるスレッドだけに本体を走らせるため、群が部分的になるとその一部のスレッドが障壁へ届きません。ジェネレーターは本体が到達する障壁からこれを判定し、そのようなシェーダーの `IComputeShaderDescriptor<T>.RequiresFullThreadGroups` を `true` として書き出すことで印します。手で宣言するものは何もなく、障壁を持つようになったシェーダーは次の構築で印されます。派遣はその印を読み、倍数でない差し渡しを `ArgumentException` で拒否します。基底のライブラリはこれを拒否しません。`ThreadGroupAlignment.AlignX`、`AlignY`、`AlignZ` が要求すべき差し渡しを答え、この要求を持たないシェーダーの差し渡しはそのまま返します。切り上げが足したスレッドは、扱っている範囲を越えた座標で本体を走らせるため、切り上げた差し渡しで派遣するシェーダーはその座標に耐える必要があります。差し渡しを3つ取らない派遣は、取らなかった軸を1に固定するため、その軸のスレッド数が2以上の群を持つシェーダーには、その軸も渡す必要があります。
- `ComputeWeave.Dxc` は `dxcompiler.dll` と `dxil.dll` を同梱するため、x64 と Arm64 以外のプロセスでは動作しません。
- `Hlsl.Abort` は Direct2D の効果では使えません。既定のコンパイル指定が要求する効果のリンクはシェーダーをライブラリとして構築しますが、FXC はそこで `abort` を受け付けません。リンクを外して構築した効果は、コンパイルできても読み込みに失敗します。
- メモリバリアは Direct2D の画素シェーダーでは使えません。同期する群が無いためで、どの画素シェーダーのプロファイルでも FXC が断る5つは、生成器が `CMPWD2D0102` を呼び出しの位置に出します。`Hlsl.DeviceMemoryBarrier` は `ps_4_0` 以上で建つので FXC に任せ、level 9 のプロファイルでは FXC が断ります。

---

Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
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 101 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 102 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 Expand Up @@ -477,6 +477,7 @@ The companion Direct2D package reaches the same end by a different route, so do
- A shader that reaches `AllMemoryBarrierWithGroupSync`, `DeviceMemoryBarrierWithGroupSync` or `GroupMemoryBarrierWithGroupSync` can only be dispatched over an extent that is a whole multiple of its thread group size on every axis. The dispatch rounds the extent up to whole groups and the entry point runs the body only for the threads inside the requested range, so a partial group would leave some of its threads short of the barrier. The generator decides this from the barriers the body reaches and marks such a shader by writing `IComputeShaderDescriptor<T>.RequiresFullThreadGroups` as `true`, so there is nothing to declare by hand and a shader that grows a barrier is marked by the next build. The dispatch reads that mark and rejects a partial extent with `ArgumentException`. The base library does not reject it. `ThreadGroupAlignment.AlignX`, `AlignY` and `AlignZ` answer with the extent to ask for, and leave the extent of a shader that carries no such requirement as it is. The threads the rounding adds run the body over coordinates past the extent being worked on, so a shader dispatched over a rounded extent has to hold for them. A dispatch taking fewer than three extents fixes the ones it does not take at one, so a shader whose thread group is deeper than one thread on those axes has to be given them as well.
- `ComputeWeave.Dxc` bundles `dxcompiler.dll` and `dxil.dll` and therefore runs only in x64 and Arm64 processes.
- `Hlsl.Abort` cannot be used in a Direct2D effect. Effect linking, which the default compile options request, builds the shader as a library, and FXC rejects `abort` there; an effect built without that library compiles but then fails to load.
- The memory barriers cannot be used in a Direct2D pixel shader, which has no thread group to synchronize. The generator reports `CMPWD2D0102` at the call for the five FXC refuses under every pixel shader profile; `Hlsl.DeviceMemoryBarrier` compiles under `ps_4_0` and above and is left to FXC, which refuses it under the level 9 profiles.

---

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,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)
Original file line number Diff line number Diff line change
Expand Up @@ -1557,4 +1557,24 @@ partial class DiagnosticDescriptors
isEnabledByDefault: true,
description: "HLSL has no recursion, so the shader compiler refuses a function that reaches itself through the functions it calls. Without this the call is written out as it stands and the shader compiler answers at the shader type, naming the function in the generated HLSL, which for a local function or a method of a custom type is a name the generator gave it. What is reported is every call on a cycle, at the call as the author wrote it, so a cycle through two declarations is reported at both of the calls closing it and removing either one resolves both. The calls followed are the ones the generated HLSL holds: a method of the shader, a method or constructor of another type the shader reaches, and a local function, which is written out whether or not it is called.",
helpLinkUri: "https://github.com/routersys/ComputeWeave");

/// <summary>
/// Gets a <see cref="DiagnosticDescriptor"/> for a thread synchronization intrinsic invoked in a pixel shader.
/// <para>
/// Format: <c>"The intrinsic {0} cannot be used in a D2D1 pixel shader (a pixel shader has no thread group to synchronize, and the pixel shader profiles refuse the call)"</c>.
/// </para>
/// </summary>
/// <remarks>
/// This has no compute counterpart. The barriers exist for the compute shaders, and the intrinsics a compute
/// shader cannot use are refused by an analyzer of that generator instead.
/// </remarks>
public static readonly DiagnosticDescriptor ThreadSynchronizationInPixelShader = new(
id: "CMPWD2D0102",
title: "Thread synchronization intrinsic in a pixel shader",
messageFormat: "The intrinsic {0} cannot be used in a D2D1 pixel shader (a pixel shader has no thread group to synchronize, and the pixel shader profiles refuse the call)",
category: "ComputeWeave.D2D1.Shaders",
defaultSeverity: DiagnosticSeverity.Error,
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");
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,28 @@ public static bool NeedsD2DRequiresScenePositionAttribute(string name)
return name is "ComputeWeave.D2D1.D2D.GetScenePosition" or "ComputeWeave.D2D1.D2D.SampleInputAtPosition";
}

/// <summary>
/// Checks whether or not a method name (previous matched with <see cref="TryGetMappedName(string, out string?)"/>)
/// is a thread synchronization intrinsic, which the pixel shader profiles refuse.
/// </summary>
/// <param name="name">The fully qualified metadata name.</param>
/// <returns>Whether the method is a thread synchronization intrinsic.</returns>
/// <remarks>
/// The shared <c>Hlsl</c> type declares six barriers for the compute shaders. Five of them were measured to be
/// refused by FXC with <c>X3664</c> under every profile Direct2D accepts, from <c>ps_4_0_level_9_1</c> to <c>ps_5_0</c>.
/// <c>DeviceMemoryBarrier</c> compiles under <c>ps_4_0</c>, <c>ps_4_1</c> and <c>ps_5_0</c> and is refused only under
/// the two level 9 profiles, and the rewriting does not know the profile, so it is not listed and FXC answers for it there.
/// </remarks>
public static bool IsThreadSynchronization(string name)
{
return name is
"ComputeWeave.Hlsl.AllMemoryBarrier" or
"ComputeWeave.Hlsl.AllMemoryBarrierWithGroupSync" or
"ComputeWeave.Hlsl.DeviceMemoryBarrierWithGroupSync" or
"ComputeWeave.Hlsl.GroupMemoryBarrier" or
"ComputeWeave.Hlsl.GroupMemoryBarrierWithGroupSync";
}

/// <summary>
/// Checks whether or not a method name (previous matched with <see cref="TryGetMappedName(string, out string?)"/>)
/// maps to a function-like macro from <c>d2d1effecthelpers.hlsli</c> that requires its coordinate argument (ie. the
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
using ComputeWeave.SourceGeneration.Extensions;
using ComputeWeave.SourceGeneration.Mappings;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using static ComputeWeave.SourceGeneration.Diagnostics.DiagnosticDescriptors;

namespace ComputeWeave.SourceGeneration.SyntaxRewriters;

Expand All @@ -10,4 +14,35 @@ protected partial void TrackKnownMethodInvocation(string metadataName)
{
Requirements.NeedsD2DRequiresScenePositionAttribute |= HlslKnownMethods.NeedsD2DRequiresScenePositionAttribute(metadataName);
}

/// <summary>
/// Reports a thread synchronization intrinsic being invoked, which the pixel shader profiles refuse.
/// </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>
/// A pixel shader runs with no thread group to synchronize, so FXC refuses the call under every pixel shader
/// profile, and before this the failure landed on the shader type as the forwarded compiler error. Reporting
/// here keeps the call from reaching the compiler at all, the compilation step being skipped for a shader whose
/// rewriting produced an error, which is what lets the author see their own line.
/// </para>
/// <para>
/// 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.
/// </para>
/// </remarks>
protected bool ReportThreadSynchronizationInPixelShader(InvocationExpressionSyntax node, string metadataName, IMethodSymbol method)
{
if (!HlslKnownMethods.IsThreadSynchronization(metadataName))
{
return false;
}

Diagnostics.Add(ThreadSynchronizationInPixelShader, node, method.Name);

return true;
}
}
Loading
Loading