Skip to content

Gradient texture cache for UberSDF gradients - #192948

Closed
b-luk wants to merge 1 commit into
flutter:masterfrom
b-luk:gradient-texture-cache
Closed

b-luk wants to merge 1 commit into
flutter:masterfrom
b-luk:gradient-texture-cache

Conversation

@b-luk

@b-luk b-luk commented Sep 17, 2026 •

Copy link
Copy Markdown
Contributor

Functionality is based off of the existing TextShadowCache.

Tested this with the "Scrolling basic LinearGradient" benchmark app from #190401. I ran it 3 times. On both my Windows VM and my Mac:

  1. The ui time was unaffected
  2. The raster time goes down significantly, so that it is on par with the solid color time.

Note that this benchmark draws one single gradient multiple times. This is the best case scenario for the gradient cache. With an app that draws different gradients (different colors and/or stops), the performance improvement will not be as drastic.

On Windows (VM with no GPU):

Before:

Avg raster p50
Solid 4.901ms
Gradient 5.962ms
Full results
=== Windows linear-gradient comparison ===
mode=profile speed=1400px/s
Solid color | frames=785 fps=196.00 | ui p50/p95=0.767/1.262ms | raster p50/p90/p95/p99/max=4.986/6.246/6.656/8.629/14.918ms | raster_over_budget=784/785 (99.9%)
Linear gradient | frames=625 fps=156.00 | ui p50/p95=1.325/2.069ms | raster p50/p90/p95/p99/max=6.185/8.370/9.056/10.632/17.350ms | raster_over_budget=625/625 (100.0%)
================================================
=== Windows linear-gradient comparison ===
mode=profile speed=1400px/s
Solid color | frames=795 fps=198.50 | ui p50/p95=0.702/1.205ms | raster p50/p90/p95/p99/max=4.890/6.048/6.622/8.330/16.769ms | raster_over_budget=795/795 (100.0%)
Linear gradient | frames=667 fps=166.50 | ui p50/p95=1.262/1.905ms | raster p50/p90/p95/p99/max=5.794/7.792/8.425/9.662/21.297ms | raster_over_budget=667/667 (100.0%)
================================================
=== Windows linear-gradient comparison ===
mode=profile speed=1400px/s
Solid color | frames=799 fps=199.50 | ui p50/p95=0.707/1.213ms | raster p50/p90/p95/p99/max=4.850/6.029/6.506/8.907/29.860ms | raster_over_budget=797/799 (99.7%)
Linear gradient | frames=658 fps=164.25 | ui p50/p95=1.226/1.906ms | raster p50/p90/p95/p99/max=5.907/7.989/8.404/10.200/13.794ms | raster_over_budget=658/658 (100.0%)
================================================

After:

Avg raster p50
Solid 4.742ms
Gradient 4.777ms
Full results
=== Windows linear-gradient comparison ===
mode=profile speed=1400px/s
Solid color | frames=810 fps=202.25 | ui p50/p95=0.684/1.295ms | raster p50/p90/p95/p99/max=4.832/6.178/6.675/7.731/8.781ms | raster_over_budget=810/810 (100.0%)
Linear gradient | frames=810 fps=202.25 | ui p50/p95=1.240/1.967ms | raster p50/p90/p95/p99/max=4.878/6.136/6.601/7.496/10.044ms | raster_over_budget=803/810 (99.1%)
================================================
=== Windows linear-gradient comparison ===
mode=profile speed=1400px/s
Solid color | frames=835 fps=208.50 | ui p50/p95=0.636/1.184ms | raster p50/p90/p95/p99/max=4.739/5.836/6.245/7.000/9.087ms | raster_over_budget=835/835 (100.0%)
Linear gradient | frames=832 fps=207.75 | ui p50/p95=1.196/1.955ms | raster p50/p90/p95/p99/max=4.732/5.925/6.292/7.166/9.001ms | raster_over_budget=828/832 (99.5%)
================================================
=== Windows linear-gradient comparison ===
mode=profile speed=1400px/s
Solid color | frames=850 fps=212.25 | ui p50/p95=0.623/1.141ms | raster p50/p90/p95/p99/max=4.654/5.795/6.153/6.956/8.610ms | raster_over_budget=849/850 (99.9%)
Linear gradient | frames=829 fps=207.00 | ui p50/p95=1.205/1.942ms | raster p50/p90/p95/p99/max=4.721/5.998/6.351/7.474/8.433ms | raster_over_budget=823/829 (99.3%)
================================================

On Mac (my local dev machine, M3 MacBook Pro):

Before:

Avg raster p50
Solid 1.235ms
Gradient 2.670ms
Full results
Note: This says "Windows" because that's hard-coded in the benchmark app. But this was run on my Mac.

flutter: === Windows linear-gradient comparison ===
flutter: mode=profile speed=1400px/s
flutter: Solid color | frames=240 fps=59.75 | ui p50/p95=1.103/1.757ms | raster p50/p90/p95/p99/max=1.216/1.611/1.689/1.951/1.999ms | raster_over_budget=0/240 (0.0%)
flutter: Linear gradient | frames=240 fps=59.75 | ui p50/p95=1.438/1.776ms | raster p50/p90/p95/p99/max=2.507/3.065/3.222/3.662/3.785ms | raster_over_budget=0/240 (0.0%)
flutter: ================================================
flutter: === Windows linear-gradient comparison ===
flutter: mode=profile speed=1400px/s
flutter: Solid color | frames=240 fps=59.75 | ui p50/p95=0.959/1.560ms | raster p50/p90/p95/p99/max=1.239/1.641/1.768/1.884/1.975ms | raster_over_budget=0/240 (0.0%)
flutter: Linear gradient | frames=240 fps=59.75 | ui p50/p95=1.548/2.355ms | raster p50/p90/p95/p99/max=2.763/3.289/3.566/3.747/3.887ms | raster_over_budget=0/240 (0.0%)
flutter: ================================================
flutter: === Windows linear-gradient comparison ===
flutter: mode=profile speed=1400px/s
flutter: Solid color | frames=240 fps=59.75 | ui p50/p95=0.910/1.543ms | raster p50/p90/p95/p99/max=1.250/1.691/1.807/2.006/2.061ms | raster_over_budget=0/240 (0.0%)
flutter: Linear gradient | frames=240 fps=59.75 | ui p50/p95=1.542/2.362ms | raster p50/p90/p95/p99/max=2.740/3.326/3.628/3.947/4.257ms | raster_over_budget=0/240 (0.0%)
flutter: ================================================

After:

Avg raster p50
Solid 1.278ms
Gradient 1.232ms
Full results
Note: This says "Windows" because that's hard-coded in the benchmark app. But this was run on my Mac.

flutter: === Windows linear-gradient comparison ===
flutter: mode=profile speed=1400px/s
flutter: Solid color | frames=240 fps=59.75 | ui p50/p95=1.085/1.653ms | raster p50/p90/p95/p99/max=1.282/1.713/1.826/1.916/1.994ms | raster_over_budget=0/240 (0.0%)
flutter: Linear gradient | frames=240 fps=59.75 | ui p50/p95=1.605/2.995ms | raster p50/p90/p95/p99/max=1.248/1.482/1.533/1.703/1.836ms | raster_over_budget=0/240 (0.0%)
flutter: ================================================
flutter: === Windows linear-gradient comparison ===
flutter: mode=profile speed=1400px/s
flutter: Solid color | frames=240 fps=59.75 | ui p50/p95=0.970/1.434ms | raster p50/p90/p95/p99/max=1.284/1.756/1.822/1.899/2.174ms | raster_over_budget=0/240 (0.0%)
flutter: Linear gradient | frames=240 fps=59.75 | ui p50/p95=1.543/2.981ms | raster p50/p90/p95/p99/max=1.250/1.510/1.573/1.691/1.776ms | raster_over_budget=0/240 (0.0%)
flutter: ================================================
flutter: === Windows linear-gradient comparison ===
flutter: mode=profile speed=1400px/s
flutter: Solid color | frames=240 fps=59.75 | ui p50/p95=1.029/1.451ms | raster p50/p90/p95/p99/max=1.269/1.635/1.706/1.857/1.982ms | raster_over_budget=0/240 (0.0%)
flutter: Linear gradient | frames=240 fps=59.75 | ui p50/p95=1.528/2.884ms | raster p50/p90/p95/p99/max=1.198/1.469/1.525/1.588/1.673ms | raster_over_budget=0/240 (0.0%)
flutter: ================================================

Pre-launch Checklist

If you need help, consider asking for advice on the #hackers-new channel on Discord.

If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance.

Note: The Flutter team is currently trialing the use of Gemini Code Assist for GitHub. Comments from the gemini-code-assist bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed.

@github-actions github-actions Bot added engine flutter/engine related. See also e: labels. e: impeller Impeller rendering backend issues and features requests labels Sep 17, 2026
@b-luk b-luk added the CICD Run CI/CD label Sep 17, 2026
@b-luk

b-luk commented Sep 18, 2026

Copy link
Copy Markdown
Contributor Author

@b-luk
b-luk marked this pull request as ready for review September 18, 2026 20:11

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a GradientTextureCache to cache gradient ramp textures based on their colors and stops, reducing the overhead of recreating textures. The cache is integrated into ContentContext and managed per frame. The review feedback suggests optimizing the cache lookup by implementing heterogeneous lookup with absl::flat_hash_map and a GradientKeyView to avoid vector copies and heap allocations on cache hits.

Comment on lines +56 to +74
/// @brief The colors and stops that define a gradient ramp.
struct GradientKey {
std::vector<Color> colors;
std::vector<Scalar> stops;

bool operator==(const GradientKey&) const = default;

template <typename H>
friend H AbslHashValue(H h, const GradientKey& key) {
return H::combine(std::move(h), key.colors, key.stops);
}
};

struct GradientTextureCacheData {
std::shared_ptr<Texture> texture;
bool used_this_frame = true;
};

absl::flat_hash_map<GradientKey, GradientTextureCacheData> entries_;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

To avoid copying the colors and stops vectors on every cache lookup (which causes heap allocations in a hot rendering path), we can use heterogeneous lookup with absl::flat_hash_map. By defining a GradientKeyView and transparent hash/equal structures, we can perform lookups using references to the existing vectors without copying them.

  struct GradientKeyView {
    const std::vector<Color>& colors;
    const std::vector<Scalar>& stops;

    template <typename H>
    friend H AbslHashValue(H h, const GradientKeyView& key) {
      return H::combine(std::move(h), key.colors, key.stops);
    }
  };

  /// @brief The colors and stops that define a gradient ramp.
  struct GradientKey {
    std::vector<Color> colors;
    std::vector<Scalar> stops;

    bool operator==(const GradientKey&) const = default;

    template <typename H>
    friend H AbslHashValue(H h, const GradientKey& key) {
      return H::combine(std::move(h), key.colors, key.stops);
    }
  };

  struct GradientKeyHash {
    using is_transparent = void;
    size_t operator()(const GradientKey& key) const {
      return absl::Hash<GradientKey>{}(key);
    }
    size_t operator()(const GradientKeyView& key) const {
      return absl::Hash<GradientKeyView>{}(key);
    }
  };

  struct GradientKeyEqual {
    using is_transparent = void;
    bool operator()(const GradientKey& lhs, const GradientKey& rhs) const {
      return lhs.colors == rhs.colors && lhs.stops == rhs.stops;
    }
    bool operator()(const GradientKey& lhs, const GradientKeyView& rhs) const {
      return lhs.colors == rhs.colors && lhs.stops == rhs.stops;
    }
  };

  struct GradientTextureCacheData {
    std::shared_ptr<Texture> texture;
    bool used_this_frame = true;
  };

  absl::flat_hash_map<GradientKey,
                      GradientTextureCacheData,
                      GradientKeyHash,
                      GradientKeyEqual>
      entries_;

Comment on lines +25 to +46
std::shared_ptr<Texture> GradientTextureCache::Lookup(
const std::shared_ptr<Context>& context,
const std::vector<Color>& colors,
const std::vector<Scalar>& stops) {
GradientKey key{.colors = colors, .stops = stops};
auto it = entries_.find(key);
if (it != entries_.end()) {
it->second.used_this_frame = true;
return it->second.texture;
}

GradientData gradient_data = CreateGradientBuffer(colors, stops);
std::shared_ptr<Texture> texture =
CreateGradientTexture(gradient_data, context);
if (!texture) {
return nullptr;
}

entries_[std::move(key)] =
GradientTextureCacheData{.texture = texture, .used_this_frame = true};
return texture;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

Update the Lookup implementation to use GradientKeyView for the cache lookup, avoiding vector copies on cache hits. We only construct and copy into a GradientKey when inserting a new entry on a cache miss.

std::shared_ptr<Texture> GradientTextureCache::Lookup(
    const std::shared_ptr<Context>& context,
    const std::vector<Color>& colors,
    const std::vector<Scalar>& stops) {
  GradientKeyView key{.colors = colors, .stops = stops};
  auto it = entries_.find(key);
  if (it != entries_.end()) {
    it->second.used_this_frame = true;
    return it->second.texture;
  }

  GradientData gradient_data = CreateGradientBuffer(colors, stops);
  std::shared_ptr<Texture> texture =
      CreateGradientTexture(gradient_data, context);
  if (!texture) {
    return nullptr;
  }

  entries_[GradientKey{.colors = colors, .stops = stops}] =
      GradientTextureCacheData{.texture = texture, .used_this_frame = true};
  return texture;
}

@b-luk

b-luk commented Sep 22, 2026

Copy link
Copy Markdown
Contributor Author

After discussion with @gaaclarke, I am closing this out. For improving this performance, we will prefer other approaches rather than caching.

@b-luk b-luk closed this Sep 22, 2026
@krll-kov

Copy link
Copy Markdown
Contributor

After discussion with @gaaclarke, I am closing this out. For improving this performance, we will prefer other approaches rather than caching.

Just in case it's helpful, i tested this in the morning on real hardware, while profiling same gradients issue and it increased performance a lot. AMD Ryzen AI 9 HX 370 with Radeon 890M. The app is a production desktop app with a full-window gradient/shader background, gradient fades on scrollables and network images. Engine: master e619960, profile build, the only change is this PR.

scenario raster avg base / PR p90 base / PR max base / PR
scroll main screen 5.96 / 3.54 6.86 / 3.89 11.4 / 7.8
tabs 4.25 / 3.95 5.55 / 4.40 13.0 / 13.1
scroll stats 3.15 / 2.61 3.42 / 2.82 5.3 / 3.8
scroll details 3.10 / 2.30 3.38 / 2.62 5.6 / 4.7
comments 3.13 / 2.45 3.41 / 2.66 4.2 / 4.2
dialog 4.16 / 3.58 5.61 / 5.04 6.9 / 6.5
bottom sheet 2.95 / 2.31 3.23 / 2.55 5.1 / 4.2
settings 2.68 / 2.27 2.96 / 2.50 6.0 / 4.9
profile 4.82 / 3.08 5.45 / 3.30 8.5 / 5.9

Per-frame blit passes from gradient texture uploads drop to about 0 in all scroll scenarios

@gaaclarke

Copy link
Copy Markdown
Member

@krll-kov there wasn't a lot of explanation provided in the message that closed this out. I pushed against adding yet another cache because we can get the same results by calculating the gradients in the fragment shaders when we have SSBO available. That's a better solution since it has more consistent performance (one of the design goals of Impeller) and avoid's another vector for bugs/memory issues.

SSBO hasn't been wired up for Windows yet though, so it will take some time to get around to it. We want SSBO wired up regardless because it can help in more cases.

@b-luk

b-luk commented Sep 22, 2026

Copy link
Copy Markdown
Contributor Author

For more context on doing this with SSBO:

#192962 adds SSBO-based gradients in UberSDF for backends which support SSBO. Windows runs on the GLES backend, which does not currently support SSBO. I filed #193162 to track adding SSBO support to GLES.

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

Labels

CICD Run CI/CD e: impeller Impeller rendering backend issues and features requests engine flutter/engine related. See also e: labels.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants