Skip to content

GPU: match the CPU's rounding in box blur rows and affine warp sampling - #1494

Draft
MatejKozlovsky wants to merge 1 commit into
storytold:mainfrom
MatejKozlovsky:gpu-blur-match
Draft

MatejKozlovsky wants to merge 1 commit into
storytold:mainfrom
MatejKozlovsky:gpu-blur-match

Conversation

@MatejKozlovsky

Copy link
Copy Markdown
Contributor

Depends on #1415 to show. Without #1415, 4K effect frames run out of GPU memory and fall back to the CPU, so the GPU kernels this PR changes never run in those tests. The diff itself doesn't depend on #1415.

Why

GPU and CPU renders of the same comp differ by one 8-bit level in a few pixels around blurred edges. Two sources of this are in the GPU kernels:

  1. Box blur rows. The GPU's horizontal box pass restarts its running sum every 256 pixels. The CPU keeps one running sum per row, and restarting rounds differently. Vertical passes already restart in blocks on both sides, at the same block size.
  2. Affine warp sampling. The CPU maps output pixels to source positions in f64. The GPU does it in f32, so a pixel can land on a slightly different source position and get slightly different bilinear weights.

What

GPU output changes on purpose in this PR, to match the CPU's. CPU output does not change (verified pixel for pixel).

  • Box blur: the horizontal pass runs one running sum per row (block = row width), as the CPU does. Vertical passes are unchanged.
  • Warp: the host passes what the f32 inverse matrix rounded off (f64 - f32) as a second set of coefficients. For affine warps with nearest or bilinear sampling, warp_blend rebuilds the f64 mapping as double-f32 (hi + lo) arithmetic, so a pixel maps to the CPU's source position and weights. A host-set factor of 1.0 keeps the GPU compiler from simplifying away the rounding-error terms. Perspective warps and other sampling modes keep the f32 path.

Results

GPU = RTX 3090, CPU = Ryzen 7 5800X. 4K, frame 0. Both builds are main + #1415, without and with this commit. The table counts pixels that differ between CPU and GPU renders.

Project Without With
Minimal blur project 1 11 3
Minimal blur project 2 24 9
Minimal blur project 3 (Gaussian) 24 11
Minimal blur project 4 8 2
Transformed layer, no blur 1 0
d sample (blur/glow) 660 134
c sample (effects) 703 490
Position / rotation / scale / static transforms, no effects 0 0
  • 8-bit PNG: every remaining difference is one level. The worst case was 1 level before and after.
  • 32-bit float EXR: the same pixels differ as in the PNGs. The largest difference fell from 7.5e-3 to 5.9e-3 on d, and the mean difference fell about 6×.
  • CPU output: identical with and without this commit in every case.
  • GPU render time: within run-to-run noise (frame 0 at 4K: d 8.7 / 9.1 s, c 8.0 / 7.6 s).
  • What's left: the remaining differences come from other kernels and are not addressed here.

Testing

  • cargo fmt -p effectcraft-gpu -- --check: clean.
  • cargo clippy -p effectcraft-gpu --all-targets -- -D warnings: clean.
  • cargo test -p effectcraft-gpu: 212 passed, 1 ignored.

@MatejKozlovsky
MatejKozlovsky marked this pull request as draft October 18, 2026 12:08

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant