Skip to content

[Shaders] fix: an integer operation keeps its operands' types in a float context - #3468

Merged
xen2 merged 3 commits into
stride3d:masterfrom
Nicogo1705:sdsl-integer-division
Sep 29, 2026
Merged

xen2 merged 3 commits into
stride3d:masterfrom
Nicogo1705:sdsl-integer-division

Conversation

@Nicogo1705

@Nicogo1705 Nicogo1705 commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

Note

Follow-up of #3467, based on it. Until #3467 is merged this PR also shows its two commits; the changes of this PR are the last two: c05b216 and dba5a4e.

Why it depends on it: folding needs to know whether a literal was written without a suffix, since only those take the context's type. Before #3467 the parser could not tell: 5l and an unsuffixed 5 got the same Suffix, and a large unsuffixed value like 3000000000 could not be represented. #3467 records it (IntegerLiteral.Unsuffixed, types unsuffixed values as DXC does), and this PR adds the same flag on FloatLiteral, so 1.0f / 3 is a float operation while 1.0 / 3 is folded in double.

PR Details

An integer operation keeps its operands' types inside a float context, and an operation made only of unsuffixed literals is folded as HLSL does.

BinaryExpression passed the expected type of its context to the operands of + - * / %, so with a uint i, float3(i / 4, 0, 0) divided in float (1 / 4 gave 0.25), 1 / 2 gave 0.5 and 5u - 6 no longer wrapped. This came with 1c722b5, so that constant expressions stay constants of their target type.

Now:

  • An operation made only of unsuffixed literals (+ - * / %, & | ^ << >>, unary - + ~, parentheses) is folded at compile time, integers in 64 bits and floats in double, and the result takes the context's type: 65536 * 65536 / 3 is 1431655765 in a uint or a float, 1 / 2 is 0 in a float, 1 << 31 >> 31 is 1. A division by zero is left to the GPU.
  • A suffixed (5u, 5l, 1.0f, 1.0h, 1.0d) or hexadecimal literal keeps its own type, so float4(5u - 6, ...) is a uint subtraction that wraps before the conversion (4294967296).
  • Every other operation takes its operands' types, and a literal still follows the other operand's.

The folding lives in LiteralFolding.cs.

Tests:

  • LiteralFoldingTests: value and type of each folded case, and the cases that must not be folded.
  • Render test LiteralOperationFolding.sdsl (D3D11 + Vulkan), one bit per failed check; it fails on the previous version of this PR and passes now.
  • IntegerOperationInFloatContext.sdsl from the first version is kept.
  • The whole Stride.Shaders.Tests suite passes (1068, with [Shaders] fix: integer suffixes after a lone 0 and on hexadecimal literals #3467).
  • The end-user sample screenshot suite (Stride.Samples.Tests, D3D11, 17 tests) passes. Frames over the LPIPS threshold (animated samples) were compared to a run of master: same drift, same images.

Note: FXC folds float(4294967295u) != 4294967296.0f in double precision (true, although the float value is 4294967296), so the render test checks those values with <.

Related Issue

None opened: found while converting the engine's shaders to C# and back, where an integer division inside a float constructor did not give the same result as HLSL. Repro: the tests above.

Types of changes

  • Docs change / refactoring / dependency upgrade
  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)

Checklist

  • My change requires a change to the documentation.
  • I have added tests to cover my changes.
  • All new and existing tests passed.
  • I have built and run the editor to try this change out. (shader compiler only; not run in the editor, the sample games were run through Stride.Samples.Tests)

⚠️ Written with AI assistance (Claude Code), driven and verified by me; needs a real review. 🤖 Generated with Claude Code

Nicogo1705 added a commit to Nicogo1705/StrideCSL that referenced this pull request Sep 22, 2026
… line

Checked on the translated class, each seen failing on Stride 4.4 far from its cause: a name the SDSL
parser reserves (sample, half, float2, params...), recursion, a shader parameter written, pixel-only
operations in a compute shader, a return inside a loop (X4555 on Direct3D 11), thread groups out of
range, a compute shader without [NumThreads], a shader replacing an engine one without
[Shader(Replaces = true)], an integer division in a float vector constructor (stride3d/stride#3468).
The round trip is unchanged: 471 of 479 engine shaders translate back, none flagged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@xen2

xen2 commented Sep 27, 2026

Copy link
Copy Markdown
Member

It seems float4(5u - 6, ...) still computes 5.0 - 6.0 = -1 (in float) as opposed to your PR description.
In HLSL/C/C++/C#, 5u - 6 returns 4294967295.

I also tried float4(65536 * 65536, ...) which DXC promote to 64bit 4294967296. SDSL also gets 4294967296 but as float instead of int64 (similar to other previous case).

{
NumberLiteral => true,
ParenthesisExpression parenthesis => IsLiteralOperation(parenthesis.Expression),
PrefixExpression { Operator: Operator.Plus or Operator.Minus } prefix and not CastExpression => IsLiteralOperation(prefix.Expression),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think and not CastExpression is not necessary as operator would be Operator.Cast.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, the prefix pattern already matches on the operator, so a cast (Operator.Cast) never matched it. The check is gone along with IsLiteralOperation: LiteralFolding.TryFold matches PrefixExpression { Operator: Plus or Minus or BitwiseNot } only, and (float)1 / 2 is covered as a case that must not be folded.

@Nicogo1705

Copy link
Copy Markdown
Contributor Author

Thanks, both cases were real.

Unsuffixed literal operations are now folded at compile time as HLSL does (64-bit integers, double floats), then take the context's type, so 65536 * 65536 / 3 gives 1431655765 and 1 / 2 gives 0 in a float. A suffixed literal keeps its own type, so float4(5u - 6, ...) now gets 4294967296. IsLiteralOperation is gone, and the redundant CastExpression check with it. Covered by LiteralFoldingTests and the LiteralOperationFolding render test; description updated.

Unrelated issue found along the way: >> always emits OpShiftRightLogical, even on a signed int (SelectBinaryOp in Builder.Expressions.cs), whereas HLSL shifts signed values arithmetically, so -16 >> 2 gives 1073741820 instead of -4. I left it out of this PR; I can open a separate one if you want.

@Nicogo1705

Copy link
Copy Markdown
Contributor Author

Rebased on #3467, which this now builds on: folding must know whether a literal was written without a suffix (only those take the context's type), and before #3467 5l and 5 had the same Suffix and 3000000000 could not be represented. #3467 records it as IntegerLiteral.Unsuffixed; this PR adds the same flag on FloatLiteral, so 1.0f / 3 stays a float operation. The last two commits are this PR's. Details in the updated description.

@xen2
xen2 force-pushed the sdsl-integer-division branch from dba5a4e to 7c57f96 Compare September 29, 2026 09:12
@xen2 xen2 added the ci-enduser Run the end-user sample screenshot + packaging suite on this PR label Sep 29, 2026
Nicogo1705 and others added 3 commits September 30, 2026 01:44
The expected type of the context was passed to the operands of + - * / %,
so in float3(i / 4, 0, 0) with a uint i the literal 4 became a float and
the division was done in float (1 / 4 gave 0.25); 1 / 2 in a float gave
0.5 and 5u - 6 no longer wrapped. It was added so that constant
expressions stay constants of their target type (1c722b5).

The context's type now only reaches + - * over literals, whose value is
the same computed in it; other operations take their operands' types, and
a literal still follows the other operand's.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
An operation made only of unsuffixed literals is now folded at compile time,
integers in 64 bits and floats in double, and the result takes the type of
its context: 65536 * 65536 / 3 is 1431655765 in a uint or a float, 1 / 2 is
0 in a float, 1 << 31 >> 31 is 1.

A suffixed or hexadecimal literal keeps its own type, so 5u - 6 is a uint
subtraction that wraps before a float context converts it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@xen2
xen2 force-pushed the sdsl-integer-division branch from 7c57f96 to 988b7aa Compare September 29, 2026 16:54
@xen2
xen2 merged commit 9b9a85f into stride3d:master Sep 29, 2026
36 checks passed
@Nicogo1705
Nicogo1705 deleted the sdsl-integer-division branch October 11, 2026 22:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci-enduser Run the end-user sample screenshot + packaging suite on this PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants