Skip to content

D2D1: D2D.SampleInputAtOffset with a compound offset expression silently samples wrong coordinates #929

Description

@rickbrew

This is due to an unparenthesized macro argument in d2d1effecthelpers.hlsli.

Claude found this while we were writing some shaders, here's its write-up:


D2D.SampleInputAtOffset(int, float2) looks like an ordinary method call from C#, but it lowers to the D2DSampleInputAtOffset function-like preprocessor macro from d2d1effecthelpers.hlsli. That macro pastes its offset argument into an arithmetic expression without parentheses:

#define D2DSampleInputAtOffset(index, offset)         InputTexture##index.Sample(InputSampler##index, __d2dstatic_uv##index.xy + offset * __d2dstatic_uv##index.zw)

So when the C# argument is a compound expression, operator precedence tears it apart after expansion. The shader compiles cleanly and runs — it just samples garbage coordinates, which makes this very unpleasant to debug.

Repro

float2 samplePosition = /* some absolute position */;
float2 scenePosition = D2D.GetScenePosition().XY;
float4 color = D2D.SampleInputAtOffset(0, samplePosition - scenePosition);

The generated HLSL invokes the macro with the expression verbatim:

D2DSampleInputAtOffset(0, samplePosition - scenePosition)

which the preprocessor expands to:

InputTexture0.Sample(InputSampler0, __d2dstatic_uv0.xy + samplePosition - scenePosition * __d2dstatic_uv0.zw)

* binds tighter than -, so only scenePosition gets scaled by the texel size while samplePosition is added as raw pixels. The intended expansion is:

InputTexture0.Sample(InputSampler0, __d2dstatic_uv0.xy + (samplePosition - scenePosition) * __d2dstatic_uv0.zw)

We hit this in production shader development for Paint.NET (the sampled output was garbage until the expression was bound to a local first). From the C# author's point of view there is no hint that a macro is involved, so normal expression semantics are silently violated.

D2DSampleInputAtPosition is also affected, just less often: its parameter expands inside (pos - __d2dstatic_scenePos.xy), which is safe for additive expressions but still tears for anything with lower precedence than - (e.g. a conditional a ? b : c).

Root cause

Classic macro hygiene bug — the parameter should be (offset) in the macro body. The macro text is identical in the Windows SDK header (10.0.26100.0\um\d2d1effecthelpers.hlsli, line 313) and in the copy that ComputeSharp.D2D1 embeds in its assembly, so the SDK deserves an upstream report too — but ComputeSharp doesn't need to wait for that.

Suggested fixes

Either (or both, for defense in depth):

  1. Patch the embedded header copy: ... + (offset) * __d2dstatic_uv##index.zw (and ((pos) - __d2dstatic_scenePos.xy) in D2DSampleInputAtPosition). One-character-class change, fixes every caller with no generator changes.
  2. Parenthesize emitted macro arguments in the source generator: when lowering D2D.* intrinsics that map to function-like macros, wrap each argument expression in parentheses (D2DSampleInputAtOffset(0, (a - b))). This also protects against any other unparenthesized macro parameters, including when compiling against the stock SDK header.

Workaround (for anyone hitting this today)

Bind the expression to a local and pass the identifier:

float2 sampleOffset = samplePosition - scenePosition;
float4 color = D2D.SampleInputAtOffset(0, sampleOffset);

Environment

  • ComputeSharp.D2D1 3.2.0 (macro text verified in the embedded header inside ComputeSharp.D2D1.dll)
  • Same macro in Windows SDK 10.0.26100.0 um\d2d1effecthelpers.hlsli line 313

Metadata

Metadata

Assignees

No one assigned

    Labels

    bug 🐛Something isn't workinguntriaged 🧰A new issue that needs initial triage

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions