Skip to content

gpui: Add dithering to linear gradient shader#51211

Merged
mikayla-maki merged 3 commits intozed-industries:mainfrom
iam-liam:gpui/gradient-dithering
Mar 30, 2026
Merged

gpui: Add dithering to linear gradient shader#51211
mikayla-maki merged 3 commits intozed-industries:mainfrom
iam-liam:gpui/gradient-dithering

Conversation

@iam-liam
Copy link
Copy Markdown
Contributor

@iam-liam iam-liam commented Mar 10, 2026

Linear gradients in dark color ranges (5-15% lightness) show visible banding due to 8-bit quantization — only ~7 distinct values exist in that range, producing hard steps instead of smooth transitions. This affects every dark theme in Zed.

What this does

Adds triangular-distributed dithering after gradient interpolation in both the Metal and HLSL fragment shaders. The noise breaks up quantization steps at the sub-pixel level, producing perceptually smooth gradients.

How it works

Two hash-based pseudo-random values (seeded from fragment position x golden ratio) are summed to produce a triangular probability distribution. This is added to the RGB channels at +/-1/255 amplitude.

  • Triangular PDF — mean-zero, so no brightness shift across the gradient
  • +/-1/255 amplitude — below perceptual threshold, invisible on bright gradients where 8-bit precision is already sufficient
  • Deterministic per-pixel — seeded from position, no temporal flickering
  • Zero-cost — a couple of fract/sin per fragment, negligible vs. the existing gradient math

Channel-specific amplitudes

Channel Amplitude Rationale
RGB ±2/255 Breaks dark-on-dark banding where adjacent 8-bit values are perceptually close
Alpha ±3/255 Alpha gradients over dark backgrounds need stronger noise — α × dark color = tiny composited steps

The higher alpha amplitude is necessary because when a semi-transparent gradient (e.g., 0.4 → 0.0 alpha) composites over a dark background, the effective visible difference per quantization step is smaller than the RGB case. ±3/255 is still well below the perceptual threshold on bright/opaque elements.

Scope

Two files changed, purely additive:

File Change
crates/gpui_macos/src/shaders.metal 13 lines after mix() in fill_color()
crates/gpui_windows/src/shaders.hlsl 13 lines after lerp() in gradient_color()

No changes to Rust code, no API changes, no new dependencies.

Screenshots

gradient_dithering_before

Before

gradient_dithering_after

After

Test plan

This is a shader-level fix; no Rust test harness exists for visual output. Manual testing is appropriate here. Visual regression tests cover UI layout, not sub-pixel rendering quality.

Manual (macOS):

  • Dark gradients (5-13% lightness range) — banding eliminated
  • Bright gradients — no visible difference (dither amplitude below precision threshold)
  • Oklab and sRGB color spaces — both paths dithered
  • Solid colours, pattern fills, checkerboard — unaffected (dither only applies to LinearGradient case)
  • Alpha gradients (semi-transparent over dark bg) — banding eliminated with alpha dithering
  • Path gradients (paint_path) — same fill_colour() function, dithering applies

Windows: HLSL change is identical logic with HLSL built-ins (frac/lerp vs fract/mix) — not tested locally.

Release Notes:

  • Improved linear gradient rendering by adding dithering to eliminate visible banding in dark color ranges

Liam

@cla-bot cla-bot bot added the cla-signed The user has signed the Contributor License Agreement label Mar 10, 2026
@zed-community-bot zed-community-bot bot added the first contribution the author's first pull request to Zed. NOTE: the label application is automated via github actions label Mar 10, 2026
@iam-liam iam-liam force-pushed the gpui/gradient-dithering branch 2 times, most recently from c803f9e to 0a16a31 Compare March 10, 2026 17:06
@iam-liam iam-liam marked this pull request as ready for review March 10, 2026 18:17
@iam-liam iam-liam force-pushed the gpui/gradient-dithering branch from 0a16a31 to ae3a5da Compare March 11, 2026 13:27
@zelenenka zelenenka added the guild Pull requests by someone in Zed Guild. NOTE: the label application is automated via github actions label Mar 16, 2026
/// statistical properties. Note: f32 sin() on CPU vs GPU may produce
/// slightly different distributions, but the properties we test
/// (small mean, small amplitude, improved quantization) hold on both.
fn shader_dither(x: f32, y: f32) -> f32 {
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.

Let's remove the rust function and tests here, as they don't actually test the relevant code in the shaders

Linear gradients in dark color ranges (5-15% lightness) show visible
banding due to 8-bit quantization — only ~7 distinct values exist in
that range, producing hard steps instead of smooth transitions.

Adds triangular-distributed dithering (±1/255) after gradient
interpolation in both the Metal and HLSL fragment shaders. Two
hash-based random values are combined for a triangular PDF that is
mean-zero (no brightness shift) with amplitude below the perceptual
threshold.

Includes Rust unit tests verifying the dithering algorithm's statistical
properties: near-zero mean, small amplitude, and improved quantization
of dark gradients.

Liam
Increase dither amplitude and extend to alpha channel:
- RGB: ±2/255 (was ±1/255) — better for dark-on-dark compositing
- Alpha: ±3/255 (new) — breaks banding in alpha gradients over dark backgrounds

Same golden-ratio triangular PDF noise, still mean-zero (no shift).

Liam
The Rust reimplementation of the shader dither algorithm doesn't
actually test the GPU code path. Removed per reviewer feedback.

Liam
@iam-liam iam-liam force-pushed the gpui/gradient-dithering branch from ae3a5da to 9e75025 Compare March 27, 2026 10:39
@iam-liam iam-liam requested a review from mikayla-maki March 27, 2026 10:39
@iam-liam
Copy link
Copy Markdown
Contributor Author

@mikayla-maki done, removed the rust function and tests. Rebased too. @reflectronic can you review please? should be good to go.

@mikayla-maki mikayla-maki enabled auto-merge (squash) March 30, 2026 16:11
@mikayla-maki mikayla-maki merged commit a46858a into zed-industries:main Mar 30, 2026
30 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cla-signed The user has signed the Contributor License Agreement first contribution the author's first pull request to Zed. NOTE: the label application is automated via github actions guild Pull requests by someone in Zed Guild. NOTE: the label application is automated via github actions

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants